czay.dev
Writing

HTTP status codes: complete reference and most common confusion

A complete reference list of HTTP status codes — plus the actual hard choices: 301 vs 308, 401 vs 403, 422 vs 400, 502 vs 503, including how Next.js handles them under the hood.

Furkan ÖzayJune 27, 2024 · 13 min read
HTTP status codes: complete reference and most common confusion

Memorizing a list of status codes is easy; picking the right one is the hard part. The difference between "couldn't find this record" and "you don't have permission to see this record" in an API, whether a POST turns into a GET during a redirect, whether Google drops an address from its index on an error page — it all comes down to choosing the right three-digit number.

This post has two parts: first, a complete list (use it as a reference), then the codes most commonly picked wrong in practice and how they show up in Next.js.

Five classes

ClassMeaningIn Short
1xxInformationalRequest received, processing continues
2xxSuccessfulRequest successfully processed
3xxRedirectionFurther action needed to complete
4xxClient ErrorError is on the client's end
5xxServer ErrorError is on the server's end

The distinction looks simple, but there are edge cases: the client sent a valid request, the server is working correctly, but a business rule forbids it — is that 4xx or 5xx? (Answer: 4xx. Details below.)

1xx — Informational

CodeNameMeaning
100ContinueClient can continue sending the request body
101Switching ProtocolsServer is switching to protocol in Upgrade header (WebSocket handshake)
102ProcessingRequest received, no result yet (WebDAV)
103Early HintsResource preload hint via Link header before final response is ready

In practice, 103 Early Hints is the only modern 1xx that's actually useful: while the server prepares the main response, it can tell the browser "start fetching this CSS and font ahead of time."

2xx — Successful

CodeNameMeaning
200OKRequest succeeded; meaning varies by HTTP method
201CreatedNew resource created (usually POST/PUT)
202AcceptedReceived but not processed yet — for async jobs
203Non-Authoritative InformationMetadata does not come from origin server (rare; prefer 200)
204No ContentSuccessful, no body — headers can be meaningful
205Reset ContentClient should reset document view
206Partial ContentPartial response to Range request (video streaming, resuming downloads)
207Multi-StatusSeparate status for multiple resources (WebDAV)
208Already ReportedAvoids re-enumerating members (WebDAV)
226IM UsedResponse returned via delta encoding

Tip: Use 201 and 204

Most APIs return 200 for everything. But when a creation endpoint returns 201 Created + a Location header, and a deletion endpoint returns 204 No Content, the client immediately knows what happened without having to parse a body.

3xx — Redirection

CodeNamePreserves method?Permanent?
300Multiple Choices——
301Moved PermanentlyNo (browsers change POST to GET)Permanent
302FoundNo (POST → GET)Temporary
303See OtherNo — always changes to GETTemporary
304Not Modified— (cached response)—
305Use Proxy⚠️ Deprecated—
306(unused)⚠️ Reserved—
307Temporary RedirectYesTemporary
308Permanent RedirectYesPermanent

This entire table boils down to one distinction: is the HTTP method preserved after redirecting?

  • 301 / 302 are legacy codes. Browsers convert POST requests to GET on these — even if the spec didn't strictly say so, that's the de facto standard.
  • 307 / 308 were introduced to clear up this ambiguity: whatever method was used stays unchanged. If you redirect a POST, it goes as a POST.
  • 303 does the exact opposite on purpose: it forces the method to GET regardless of what came in.

Explanation: What is 303 for?

When a user submits a form, you want to redirect them to a result page. If you use 307, they will hit the result page with a POST request too; if they refresh the page, the browser asks "do you want to resubmit the form?" and you might get duplicate records. 303 prevents this trap by forcing the request to GET. This pattern is called POST/Redirect/GET and is critical in workflows like payment callbacks.

Warning: SEO impact

Use 301/308 for permanent moves; search engines will drop the old URL from their index and transfer link equity to the new one. If you use 302/307, the old URL stays indexed. Getting this choice wrong during site migrations is one of the most common causes of traffic loss.

4xx — Client Error

CodeNameMeaning
400Bad RequestRequest is malformed: invalid syntax, body cannot be parsed
401UnauthorizedActually means "unauthenticated" — login required
402Payment RequiredPayment required; no standard usage
403ForbiddenServer knows who you are, but you don't have permission
404Not FoundResource missing — or you want to hide its existence
405Method Not AllowedMethod is recognized but not supported on this resource
406Not AcceptableContent negotiation produced no acceptable format
407Proxy Authentication RequiredLike 401, but for a proxy server
408Request TimeoutIdle connection closed by server
409ConflictRequest conflicts with current state of resource
410GonePermanently deleted, address no longer exists
411Length RequiredContent-Length header required
412Precondition FailedPrecondition in conditional request failed
413Content Too LargeRequest body exceeds server limits
414URI Too LongTarget URL is too long
415Unsupported Media TypeRequest payload format is unsupported
416Range Not SatisfiableRequested range cannot be fulfilled
417Expectation FailedExpectation in Expect header could not be met
418I'm a teapotJoke code (RFC 2324) — teapots can't brew coffee
421Misdirected RequestRequest sent to wrong server
422Unprocessable ContentSyntax is correct, but semantic content is invalid
423LockedResource is locked (WebDAV)
424Failed DependencyPrevious request failed (WebDAV)
425Too EarlyRefuses to process request that might be replayed (TLS 1.3 early data)
426Upgrade RequiredClient must upgrade protocol
428Precondition RequiredConditional request required — prevents "lost update" problem
429Too Many RequestsRate limit exceeded
431Request Header Fields Too LargeHeaders are too large
451Unavailable For Legal ReasonsAccess blocked for legal reasons

401 or 403?

The most frequently confused pair. Their names are misleading, because 401 actually means "unauthenticated":

  • 401 — I don't know who you are. Log in. (No session or invalid token.)
  • 403 — I know who you are, but you can't access this resource. (A user with the user role trying to access /admin.)

Tip: A third option: 404

Sometimes even 403 reveals too much. Saying "this resource exists, but you can't see it" leaks information. Returning 404 to completely conceal a resource's existence — like GitHub does for private repositories — is a legitimate approach.

400 or 422?

  • 400 — I couldn't parse the request. Corrupted JSON, missing required field, wrong data type.
  • 422 — I parsed it and the format is valid, but the meaning is invalid. "End date cannot be before start date", "this email is already registered".

In other words: syntax error is 400, business rule violation is 422. In practice, many APIs return 400 for both, which is fine — but if you separate them, client applications can handle "fix the form" vs "fix the request format" without extra code.

409 and 410

  • 409 Conflict — The request conflicts with the current state of the resource. Concurrent editing version conflict, trying to create a resource with a slug that already exists.
  • 410 Gone — The resource existed, was permanently deleted, and won't come back. The difference from 404 is certainty: search engines deindex a 410 much faster. If you're not sure, stick to 404.

429 and Retry-After

If you implement rate limiting, returning 429 alone is only half the job. Send a Retry-After header too — so the client knows how long to wait instead of blindly retrying.

Example: Rate limiting in a Next.js Route Handler

TypeScript
export async function POST(req: Request) {
	const allowed = await rateLimit(req);
 
	if (!allowed) {
		return new Response(
			JSON.stringify({ error: "Çok fazla istek gönderdiniz." }),
			{
				status: 429,
				headers: {
					"Content-Type": "application/json",
					"Retry-After": "60", // seconds
				},
			}
		);
	}
 
	// ...
}

451

Used when content is blocked for legal reasons. Named after Ray Bradbury's novel Fahrenheit 451 — the temperature at which paper burns.

5xx — Server Error

CodeNameMeaning
500Internal Server ErrorGeneric error; server encountered an unexpected condition
501Not ImplementedServer does not support the request method
502Bad GatewayProxy received an invalid response from upstream server
503Service UnavailableServer temporarily unable to handle request (maintenance, overload)
504Gateway TimeoutUpstream server failed to respond in time
505HTTP Version Not SupportedHTTP version not supported
506Variant Also NegotiatesServer configuration error (circular negotiation)
507Insufficient StorageStorage space exhausted (WebDAV)
508Loop DetectedInfinite loop detected (WebDAV)
510Not ExtendedRequired HTTP extension not supported
511Network Authentication RequiredClient needs to authenticate to gain network access (wifi portal)

502, 503, 504 — which failure is which?

These three are the fastest way to diagnose server deployment issues:

  • 502 Bad Gateway — Nginx is up, but the app behind it has crashed or isn't responding on that port. Check your app logs.
  • 503 Service Unavailable — Server is up, but refuses to serve traffic right now: maintenance mode, system overload, no instance available in pool. Should be sent with Retry-After and must not be cached.
  • 504 Gateway Timeout — Upstream app exists but is too slow; proxy gave up waiting. Look for slow queries or slow external API calls.

Caution: Don't return 200 for maintenance mode

Serving a "We're down for maintenance" page with status 200 tricks search engines into thinking that page is your actual content. Maintenance pages should return 503 — search engines won't penalize your ranking for short outages.

Status codes in Next.js

The framework chooses some status codes for you, and knowing what it picks under the hood helps.

Important: Redirect functions

  • redirect() → 307 (temporary, preserves method)
  • permanentRedirect() → 308 (permanent, preserves method)
  • redirect() inside Server Action form submission → 303 (so the browser follows up with GET; if JS is active, client-side navigation handles it anyway)
  • NextResponse.redirect() → defaults to 307

Next.js picks 307/308 over 302/301 because of the method preservation issue mentioned above: a browser receiving 302 converts POST to GET, turning a POST to /users into a GET to /people.

If you are making a permanent address change, use permanentRedirect() instead of redirect() — or set permanent: true inside your next.config.ts redirects() config. Otherwise, search engines won't deindex the old address.

Calling notFound() returns 404 and renders your not-found.tsx file.

Example: Correct status codes in a Route Handler

TypeScript
import { NextResponse } from "next/server";
 
export async function POST(req: Request) {
	const body = await req.json().catch(() => null);
 
	if (!body) {
		return NextResponse.json({ error: "Geçersiz JSON" }, { status: 400 });
	}
 
	const parsed = schema.safeParse(body);
	if (!parsed.success) {
		// Valid format, invalid meaning
		return NextResponse.json(
			{ error: "Doğrulama hatası", issues: parsed.error.issues },
			{ status: 422 }
		);
	}
 
	const session = await auth();
	if (!session) {
		return NextResponse.json({ error: "Giriş gerekli" }, { status: 401 });
	}
	if (session.user.role !== "admin") {
		return NextResponse.json({ error: "Yetkiniz yok" }, { status: 403 });
	}
 
	const created = await db.post.create({ data: parsed.data });
 
	return NextResponse.json(created, {
		status: 201,
		headers: { Location: `/api/posts/${created.id}` },
	});
}

Quick decision guide

Summary: Which code should I return?

  • Created a record → 201 + Location
  • Deleted, nothing to return → 204
  • URL permanently changed → 308 (301 also acceptable for SEO)
  • Redirecting to result page after form submit → 303
  • Unauthenticated → 401
  • Authenticated but unauthorized → 403
  • Need to hide record existence → 404
  • Corrupted JSON → 400
  • Valid JSON, business rule violation → 422
  • Concurrent edit conflict → 409
  • Rate limited → 429 + Retry-After
  • Down for maintenance → 503 + Retry-After
  • Unexpected error → 500 (write the actual reason to logs, not to the user)

One final note: never expose stack traces to users in error responses. Status codes tell the client what action to take; granular details belong in server logs.

Note: Sources

The status code list is based on the MDN HTTP response status codes reference, and Next.js behaviors follow the redirect documentation.