303 See Other
What is HTTP 303 See Other?
When you submit a form on a website - like sending a message or uploading a pict...
Explain Like I’m 3
You gave someone a picture you drew. They say ‘Thank you! Now go look over there to see something cool!’ So you go look at the cool thing. You’re not giving them another picture - you’re just looking!
Example: You put your toy in the toy box. Someone says ‘Good job! Now go see what’s on the table!’ You walk over and look at what’s on the table. You’re not putting more toys away - you already did that!
Explain Like I’m 5
When you submit a form on a website - like sending a message or uploading a picture - the website processes it and then says ‘303 See Other - I got your submission! Now let me show you this other page!’ Your browser then loads that other page to show you. The important part: even if you submitted something by pressing ‘Send’, when you go to the new page, your browser just looks at it - it doesn’t try to submit again. This prevents accidentally sending the same form twice if you press the back button or refresh the page. It’s like after you turn in your homework, your teacher shows you the answers page instead of making you turn in homework again.
Example: You fill out a ‘Contact Us’ form on a website and click Submit. The website receives your message and responds with ‘303 See Other’ pointing to a ‘Thank You’ page. Your browser loads the Thank You page. If you accidentally hit refresh or back, you just see the Thank You page again - you don’t re-send your message!
Jr. Developer
HTTP 303 See Other is a redirect status code that explicitly tells the client to retrieve the resource at the Location header using GET, regardless of the original request method. This is the cornerstone of the POST-Redirect-GET (PRG) pattern used to prevent duplicate form submissions. After processing a POST, PUT, or DELETE request, return 303 with a Location header pointing to a result page. The browser follows the redirect using GET. If the user refreshes, they GET the result page again instead of re-submitting the form. 303 was introduced in HTTP/1.1 (RFC 2616) to clarify ambiguous behavior with 302 - while 302 was supposed to preserve method, browsers actually changed POST to GET. 303 makes this GET conversion explicit and guaranteed. Key difference: 302 is ambiguous (may or may not change method), 303 always changes to GET, 307 never changes method. Always use 303 (not 302) for modern web applications implementing PRG pattern.
Example: User submits a registration form via POST /register. Your server creates the user account, then returns 303 with Location: /welcome. The browser GETs /welcome and displays a welcome message. If the user refreshes the welcome page, they just see the welcome message again - they don’t create another account.
Code Example
// Express.js implementing POST-Redirect-GET with 303app.post('/register', async (req, res) => { const { username, email, password } = req.body;
// Validate input if (!username || !email || !password) { return res.status(400).render('register', { error: 'All fields required' }); }
// Create user account const user = await db.createUser({ username, email, password });
// Save user ID to session req.session.userId = user.id;
// 303 See Other - browser will GET /welcome // Even if user refreshes, won't re-POST registration res.redirect(303, '/welcome');});
app.get('/welcome', (req, res) => { if (!req.session.userId) { return res.redirect(302, '/login'); }
res.render('welcome', { user: req.session.user });});
// Form submission exampleapp.post('/contact', async (req, res) => { const { name, email, message } = req.body;
// Process contact form await db.saveContactMessage({ name, email, message }); await emailService.notifyAdmin({ name, email, message });
// Redirect to thank-you page with 303 res.redirect(303, '/contact/thank-you');});
app.get('/contact/thank-you', (req, res) => { res.render('thank-you'); // Safe to refresh});Crash Course
303 See Other, defined in RFC 9110 Section 15.4.4 (obsoleting RFC 2616), indicates the server is redirecting the user agent to a different resource, as indicated by the Location header field, which is intended to provide an indirect response to the original request. The user agent MUST use GET to retrieve the representation at the redirect target, regardless of the original method. This is the semantically correct status code for implementing the POST-Redirect-GET (PRG) pattern. Historical context: HTTP/1.0’s 302 was ambiguous - spec said preserve method, browsers changed POST to GET. HTTP/1.1 introduced 303 (explicitly convert to GET), 307 (explicitly preserve method), and clarified 302 behavior. Today: use 303 after POST/PUT/DELETE to prevent duplicate submissions, 307 when method must be preserved, avoid 302 for new applications. The 303 response itself is not cacheable, but the GET request to the redirect target may be cacheable. Primary use case: form submissions where refresh should display result, not resubmit. Secondary uses: async operations (return 303 to status check URL), file uploads (redirect to upload success page), payment processing (prevent duplicate charges), REST APIs (point to newly created resource without forcing client to cache the POST response).
Example: An e-commerce checkout flow: user POSTs payment details to /checkout/pay. Server processes payment (charges card, creates order) and returns 303 with Location: /order/12345. Browser GETs /order/12345 displaying order confirmation. User refreshes to check order details - browser GETs /order/12345 again, doesn’t re-POST payment. No duplicate charge. If 302 or no redirect was used, refresh might resubmit payment, causing double charge.
Code Example
// Advanced POST-Redirect-GET patternsconst express = require('express');const app = express();
// Form submission with PRG patternapp.post('/forms/submit', async (req, res) => { const { formType, ...formData } = req.body;
// Save form submission const submission = await db.saveFormSubmission({ type: formType, data: formData, submittedAt: new Date() });
// Redirect to confirmation page using 303 // Query params allow showing submission details without POST res.redirect(303, `/forms/confirmation?id=${submission.id}`);});
app.get('/forms/confirmation', async (req, res) => { const submission = await db.getSubmission(req.query.id);
if (!submission) { return res.status(404).render('404'); }
// Safe to refresh - just displays confirmation res.render('form-confirmation', { submission });});
// REST API - create resource then redirect to itapp.post('/api/articles', async (req, res) => { const { title, content, author } = req.body;
// Create article const article = await db.createArticle({ title, content, author });
// 303 points client to the created resource // Client will GET /api/articles/123 to retrieve it res.set('Location', `/api/articles/${article.id}`); res.status(303).json({ message: 'Article created', location: `/api/articles/${article.id}` });});
app.get('/api/articles/:id', async (req, res) => { const article = await db.getArticle(req.params.id);
if (!article) { return res.status(404).json({ error: 'Article not found' }); }
res.json(article);});
// File upload with 303 redirectapp.post('/upload', upload.single('file'), async (req, res) => { const file = req.file;
// Process upload const fileRecord = await db.saveFileRecord({ originalName: file.originalname, path: file.path, size: file.size, mimetype: file.mimetype });
// Redirect to file details page res.redirect(303, `/files/${fileRecord.id}`);});
// Async operation with 303 to status endpointapp.post('/api/reports/generate', async (req, res) => { const { reportType, params } = req.body;
// Start async report generation const job = await reportQueue.add({ type: reportType, params, status: 'pending' });
// Redirect to status check endpoint res.redirect(303, `/api/reports/status/${job.id}`);});
app.get('/api/reports/status/:jobId', async (req, res) => { const job = await reportQueue.getJob(req.params.jobId);
if (job....Deep Dive
The 303 See Other status code, specified in RFC 9110 Section 15.4.4, indicates that the server is redirecting the user agent to a different resource, as indicated by the Location header field, intended to provide an indirect response to the original request. A user agent receiving this status code MUST perform a GET request to the Location URI, regardless of the original request method. The 303 status was introduced to eliminate ambiguity in redirect behavior that plagued HTTP/1.0. Per RFC 9110, a 303 response to a GET request indicates that the origin server does not have a representation of the target resource that can be transferred but has a URI of a related resource that might be of interest. For other request methods, a 303 indicates successful completion and provides a URI for retrieving a representation of the result.
Technical Details
Historical evolution of redirect method handling: HTTP/1.0 (RFC 1945) defined 302 “Moved Temporarily” and specified clients should redirect using the same method, but Netscape Navigator and Internet Explorer both converted POST to GET for usability (users expected to see results, not resubmit forms). HTTP/1.1 (RFC 2068, later RFC 2616) acknowledged this de facto behavior and introduced three distinct status codes: 302 Found (ambiguous, allows method change for backward compatibility), 303 See Other (explicitly mandates GET conversion), and 307 Temporary Redirect (explicitly forbids method change). RFC 9110 (current HTTP Semantics) clarifies that 303 MUST change method to GET, with the intent of implementing the PRG pattern.\n\nCaching semantics for 303 are explicit: the 303 redirect response itself is not cacheable (per RFC 9110 Section 15.4.4), but the GET request to the redirect target follows normal caching rules. This prevents caching the redirect relationship while allowing caching of the result. Example: POST /order returns 303 → /order/123. The redirect isn’t cached (future POSTs to /order won’t be automatically redirected), but GET /order/123 can be cached with appropriate Cache-Control headers. This is critical for forms - each submission creates a new result, so the redirect mapping mustn’t be cached, but viewing the result can be cached.\n\nPRG pattern implementation details: The POST-Redirect-GET pattern solves the “duplicate form submission” problem where refreshing after POST causes form resubmission. Without PRG, browser shows “Confirm Form Resubmission” dialog on refresh. With PRG: (1) Client POSTs form data to /submit, (2) Server processes data and returns 303 with Location: /success, (3) Browser GETs /success, (4) User refreshes - browser GETs /success again, no resubmission. The URL changes from /submit to /success, allowing users to bookmark the result. Session flash messages (temporary session data) often accompany PRG to pass success/error messages from POST handler to GET result page.\n\nREST API usage patterns for 303 diverge from typical web forms: After POST creates resource, returning 201 Created with Location header pointing to the new resource (/api/resources/123) is standard. 303 See Other is semantically appropriate when the POST doesn’t create a single resource but rather triggers an operation whose result can be viewed elsewhere. Example: POST /api/batch-import returns 303 → /api/batch-import/status/456. The client GETs the status endpoint to check import progress. Once complete, status endpoint returns links to imported resources. This pattern is common for long-running async operations where immediate response contains no useful data but checking status later does.\n\nSecurity implications of 303 redirects include CSRF vulnerabilities if the GET endpoint performs state-changing operations (violates HTTP semantics but happens), open redirect vulnerabilities if Location header comes from user input without v…
Code Example
// Production-grade 303 See Other implementation\nconst express = require('express');\nconst session = require('express-session');\nconst csrf = require('csurf');\nconst { body, validationResult } = require('express-validator');\nconst app = express();\n\n// Session and CSRF setup\napp.use(session({\n secret: process.env.SESSION_SECRET,\n resave: false,\n saveUninitialized: false,\n cookie: { httpOnly: true, secure: true, sameSite: 'lax' }\n}));\n\nconst csrfProtection = csrf();\n\n// Flash messages middleware for PRG pattern\nfunction flashMessages(req, res, next) {\n res.locals.messages = req.session.messages || {};\n req.session.messages = {};\n next();\n}\n\napp.use(flashMessages);\n\n// Helper for safe redirects (prevents open redirect)\nfunction safeRedirect303(req, res, path, flashMessage = null) {\n // Validate path is relative (starts with /)\n if (!path.startsWith('/')) {\n console.error(\`Invalid redirect path: ${path}\`);\n path = '/';\n }\n \n // Add flash message if provided\n if (flashMessage) {\n req.session.messages = {\n ...req.session.messages,\n [flashMessage.type]: flashMessage.text\n };\n }\n \n // Send 303 See Other\n res.redirect(303, path);\n}\n\n// Form submission with comprehensive PRG\napp.post('/account/update',\n csrfProtection,\n [\n body('email').isEmail().normalizeEmail(),\n body('displayName').trim().isLength({ min: 2, max: 50 })\n ],\n async (req, res) => {\n // Require authentication\n if (!req.session.userId) {\n return res.redirect(302, '/login');\n }\n \n // Validate input\n const errors = validationResult(req);\n if (!errors.isEmpty()) {\n req.session.messages = {\n error: 'Validation failed: ' + errors.array().map(e => e.msg).join(', ')\n };\n return safeRedirect303(req, res, '/account/edit');\n }\n \n const { email, displayName } = req.body;\n \n try {\n // Update user account\n const updated = await db.updateUser(req.session.userId, {\n email,\n displayName,\n updatedAt: new Date()\n });\n \n // Regenerate session with new data\n req.session.user = updated;\n \n // Redirect with success message\n safeRedirect303(req, res, '/account', {\n type: 'success',\n text: 'Account updated successfully'\n });\n } catch (error) {\n console.error('Account update failed:', error);\n \n safeRedirect303(req, res, '/account/edit', {\n type: 'error',\n text: 'Failed to update account. Please try again.'\n });\n }\n }\n);\n\napp.get('/account', (req, res) => {\n if (!req.session.userId) {\n return res.redirect(302, '/login');\n }\n \n // Render with flash messages from session\n res.render('account', {\n user: req.session.user,\n messages: res.locals.messages // Available from flashMessages middleware\n });\n});\n\n// REST API - async operation with 303 to status\napp.post('...Frequently Asked Questions
What's the difference between 303 See Other and 302 Found?
303 explicitly guarantees the redirect will be retrieved using GET, regardless of the original method. 302 is ambiguous - the spec originally said to preserve method, but browsers historically changed POST to GET. For modern applications, use 303 for the POST-Redirect-GET pattern because it's semantically clear and guaranteed. 302 is legacy and should be avoided for form submissions.
When should I use 303 See Other vs 307 Temporary Redirect?
Use 303 when you want POST/PUT/DELETE to become GET (form submissions, file uploads, payment processing - PRG pattern). Use 307 when you need to preserve the method (POST stays POST), typically for API redirects where the request body must be sent to the redirect target. 303 prevents duplicate submissions; 307 ensures method preservation.
Is the 303 redirect response cacheable?
No, the 303 response itself is not cacheable per RFC 9110. However, the GET request to the redirect target (Location URL) follows normal caching rules and may be cached with appropriate Cache-Control headers. This prevents caching the redirect relationship while allowing caching of results, which is essential for forms where each submission creates a unique result.
Should I use 303 or 201 Created after POST creates a resource?
In REST APIs, 201 Created with a Location header pointing to the new resource is standard and preferred. Use 303 when the POST triggers an operation whose result should be viewed elsewhere (async operations, batch imports, long-running tasks), or when implementing traditional web forms with the PRG pattern. For simple resource creation, 201 is more semantically correct.
How do I prevent open redirect vulnerabilities with 303?
Always validate the redirect target: (1) Only allow relative paths (starting with /), never absolute URLs from user input, (2) Whitelist allowed redirect paths, (3) Validate against known internal routes, (4) Default to a safe page (/) if validation fails. Never use user-provided URLs directly in the Location header without validation - this creates open redirect vulnerabilities attackers can exploit for phishing.
Common Causes
- Form submission completing successfully (POST-Redirect-GET pattern)
- File upload finishing, redirecting to upload confirmation page
- Payment processing completing, redirecting to order confirmation
- Account update finishing, redirecting to account settings with success message
- REST API POST creating resource, redirecting to resource URL
- Long-running async operation started, redirecting to status check endpoint
- Batch operation initiated, redirecting to progress tracking page
- Login completing successfully, redirecting to dashboard or return URL
Implementation Guidance
- Use 303 (not 302) for all POST-Redirect-GET patterns in modern applications
- Always include Location header with the redirect target URL
- Implement flash messages via session to pass success/error from POST to GET
- Validate redirect targets to prevent open redirect vulnerabilities
- Regenerate session IDs after authentication before sending 303 redirect
- Set Cache-Control: no-store on the 303 response itself (not cacheable)
- Use relative URLs in Location header when possible for better security
- For async operations, redirect to status endpoint that clients can poll
- Implement CSRF protection on POST endpoints before redirecting
- Avoid redirect chains - 303 should point to final destination, not another redirect