Skip to content
gohacks.dev
APIs

HTTP Status Codes for Go APIs in a Quick Reference

A quick reference of the HTTP status codes a Go API needs, with net/http constants, when to use each, and how to pick between 400, 401, 403, 404, 409 and 422.

6 min read
Summarize in AI
Flat illustration of a browser and a server trading a request and a response.

Status codes are the part of an API that every client library, proxy, load balancer and monitoring dashboard understands without reading your documentation. Choosing them well makes the API behave sensibly in tools you never wrote. Choosing them badly (200 with an error in the body, 500 for validation errors) turns your graphs into noise.

Tested with Go 1.25.6, constants and status text printed from net/http

This is a reference for the codes a JSON API needs, with the exact constant names from Go's net/http package. The listing below was printed by a program that reads each constant and its StatusText, so the names are not from memory. The semantics follow RFC 9110. For the design context, see REST API design in Go and the mistakes roundup.

main.goGo
package main

import (
	"fmt"
	"net/http"
)

type entry struct {
	code int
	name string // the net/http constant
}

func main() {
	codes := []entry{
		{http.StatusOK, "StatusOK"}, {http.StatusCreated, "StatusCreated"}, {http.StatusAccepted, "StatusAccepted"},
		{http.StatusNoContent, "StatusNoContent"}, {http.StatusPartialContent, "StatusPartialContent"},
		{http.StatusMovedPermanently, "StatusMovedPermanently"}, {http.StatusFound, "StatusFound"},
		{http.StatusSeeOther, "StatusSeeOther"}, {http.StatusNotModified, "StatusNotModified"},
		{http.StatusTemporaryRedirect, "StatusTemporaryRedirect"}, {http.StatusPermanentRedirect, "StatusPermanentRedirect"},
		{http.StatusBadRequest, "StatusBadRequest"}, {http.StatusUnauthorized, "StatusUnauthorized"},
		{http.StatusForbidden, "StatusForbidden"}, {http.StatusNotFound, "StatusNotFound"},
		{http.StatusMethodNotAllowed, "StatusMethodNotAllowed"}, {http.StatusNotAcceptable, "StatusNotAcceptable"},
		{http.StatusRequestTimeout, "StatusRequestTimeout"}, {http.StatusConflict, "StatusConflict"},
		{http.StatusGone, "StatusGone"}, {http.StatusLengthRequired, "StatusLengthRequired"},
		{http.StatusPreconditionFailed, "StatusPreconditionFailed"}, {http.StatusRequestEntityTooLarge, "StatusRequestEntityTooLarge"},
		{http.StatusRequestURITooLong, "StatusRequestURITooLong"}, {http.StatusUnsupportedMediaType, "StatusUnsupportedMediaType"},
		{http.StatusRequestedRangeNotSatisfiable, "StatusRequestedRangeNotSatisfiable"},
		{http.StatusUnprocessableEntity, "StatusUnprocessableEntity"}, {http.StatusLocked, "StatusLocked"},
		{http.StatusPreconditionRequired, "StatusPreconditionRequired"}, {http.StatusTooManyRequests, "StatusTooManyRequests"},
		{http.StatusRequestHeaderFieldsTooLarge, "StatusRequestHeaderFieldsTooLarge"},
		{http.StatusUnavailableForLegalReasons, "StatusUnavailableForLegalReasons"},
		{http.StatusInternalServerError, "StatusInternalServerError"}, {http.StatusNotImplemented, "StatusNotImplemented"},
		{http.StatusBadGateway, "StatusBadGateway"}, {http.StatusServiceUnavailable, "StatusServiceUnavailable"},
		{http.StatusGatewayTimeout, "StatusGatewayTimeout"},
	}
	for _, c := range codes {
		fmt.Printf("%d  %-36s %s\n", c.code, "http."+c.name, http.StatusText(c.code))
	}
	fmt.Println()
	fmt.Println("StatusText(499) is empty:", http.StatusText(499) == "", "- nginx uses 499 for a client that closed the connection, but Go does not name it")
	fmt.Println("StatusText(422):", http.StatusText(422))
}
Text
200  http.StatusOK                        OK
201  http.StatusCreated                   Created
202  http.StatusAccepted                  Accepted
204  http.StatusNoContent                 No Content
206  http.StatusPartialContent            Partial Content
301  http.StatusMovedPermanently          Moved Permanently
302  http.StatusFound                     Found
303  http.StatusSeeOther                  See Other
304  http.StatusNotModified               Not Modified
307  http.StatusTemporaryRedirect         Temporary Redirect
308  http.StatusPermanentRedirect         Permanent Redirect
400  http.StatusBadRequest                Bad Request
401  http.StatusUnauthorized              Unauthorized
403  http.StatusForbidden                 Forbidden
404  http.StatusNotFound                  Not Found
405  http.StatusMethodNotAllowed          Method Not Allowed
406  http.StatusNotAcceptable             Not Acceptable
408  http.StatusRequestTimeout            Request Timeout
409  http.StatusConflict                  Conflict
410  http.StatusGone                      Gone
411  http.StatusLengthRequired            Length Required
412  http.StatusPreconditionFailed        Precondition Failed
413  http.StatusRequestEntityTooLarge     Request Entity Too Large
414  http.StatusRequestURITooLong         Request URI Too Long
415  http.StatusUnsupportedMediaType      Unsupported Media Type
416  http.StatusRequestedRangeNotSatisfiable Requested Range Not Satisfiable
422  http.StatusUnprocessableEntity       Unprocessable Entity
423  http.StatusLocked                    Locked
428  http.StatusPreconditionRequired      Precondition Required
429  http.StatusTooManyRequests           Too Many Requests
431  http.StatusRequestHeaderFieldsTooLarge Request Header Fields Too Large
451  http.StatusUnavailableForLegalReasons Unavailable For Legal Reasons
500  http.StatusInternalServerError       Internal Server Error
501  http.StatusNotImplemented            Not Implemented
502  http.StatusBadGateway                Bad Gateway
503  http.StatusServiceUnavailable        Service Unavailable
504  http.StatusGatewayTimeout            Gateway Timeout

StatusText(499) is empty: true - nginx uses 499 for a client that closed the connection, but Go does not name it
StatusText(422): Unprocessable Entity

Success: 2xx

CodeGo constantUse it for
200StatusOKA successful read, or an update that returns the resource
201StatusCreatedA resource was created. Set Location to its URL and return the representation
202StatusAcceptedWork was accepted and will finish later. Give a status resource (async jobs)
204StatusNoContentSuccess with nothing to return: a DELETE, or a PUT that echoes nothing. No body allowed
206StatusPartialContentA range request (range requests)

A POST that creates returns 201, not 200. A DELETE returns 204 (or 200 with a body if you return the deleted resource). A list with no results is a 200 with an empty array, never a 404.

Redirection and caching: 3xx

CodeGo constantUse it for
301 / 308StatusMovedPermanently / StatusPermanentRedirectThe resource has a new permanent URL. 308 keeps the method; 301 may turn a POST into a GET
302 / 307StatusFound / StatusTemporaryRedirectA temporary move. 307 keeps the method
303StatusSeeOtherAfter a POST, send the client to a GET of the result
304StatusNotModifiedA conditional request matched the ETag or date (ETags). No body

APIs redirect rarely. When you must move an endpoint, prefer 308 so clients that POST keep working.

Client errors: 4xx

CodeGo constantUse it for
400StatusBadRequestThe request could not be understood: malformed JSON, a wrong type, an unparseable parameter
401StatusUnauthorizedNo credentials or invalid ones. Send WWW-Authenticate
403StatusForbiddenAuthenticated, but not allowed to do this
404StatusNotFoundNo such resource, or one the caller may not know exists
405StatusMethodNotAllowedThe method is not supported on this path. Send Allow (the Go 1.22 mux does)
406StatusNotAcceptableYou cannot produce what Accept asks for
408StatusRequestTimeoutThe client was too slow to send the request
409StatusConflictThe request conflicts with current state: duplicate unique value, wrong version
410StatusGoneDeliberately removed (deprecation)
411, 413, 414StatusLengthRequired, StatusRequestEntityTooLarge, StatusRequestURITooLongA length is missing, the body is too large, the URL is too long
412 / 428StatusPreconditionFailed / StatusPreconditionRequiredIf-Match did not match / a conditional header is mandatory (optimistic concurrency)
415StatusUnsupportedMediaTypeThe Content-Type of the body is not accepted
416StatusRequestedRangeNotSatisfiableA range outside the resource
422StatusUnprocessableEntityThe syntax is fine but the values fail validation
423StatusLockedThe resource is locked
429StatusTooManyRequestsRate limited. Send Retry-After (rate limiting)
431StatusRequestHeaderFieldsTooLargeHeaders too big
451StatusUnavailableForLegalReasonsBlocked for legal reasons

Server errors: 5xx

CodeGo constantUse it for
500StatusInternalServerErrorAn unexpected failure: a bug, a failed assumption. Generic body, details in the log
501StatusNotImplementedThe method or feature is not supported at all
502StatusBadGatewayA proxy or gateway got a bad answer from upstream
503StatusServiceUnavailableOverloaded, in maintenance or shedding load. Send Retry-After
504StatusGatewayTimeoutA proxy or gateway timed out waiting for upstream

Clients and retry libraries treat 5xx as "maybe try again". Do not return 500 for things the client can fix, and do not return 4xx for your own bugs.

The confusions, settled

401 or 403? 401 means "I do not know who you are": no token, expired token, bad signature. 403 means "I know who you are, and you may not". A client should react to 401 by authenticating and to 403 by giving up or asking for access.

403 or 404 for someone else's object? Return 404. A 403 confirms the object exists, which is information leakage (BOLA). Use 403 when the caller can already see the object but not change it.

400 or 422? 400 for requests the server could not parse or that violate the protocol of the API (invalid JSON, wrong type). 422 for requests that parsed and violate a business or validation rule. Some teams use only 400 and put details in the body. Either works if it is consistent and documented.

409 or 422? 409 when the failure depends on the current state of the system, such as a duplicate email or a version mismatch. 422 when the request is invalid on its own.

200 or 204 for a successful PUT? 200 with the updated resource is friendlier to clients; 204 saves bytes. Choose one per API.

201 or 202? 201 when the resource exists now, 202 when it will exist later.

404 or 410? 404 for "not found"; 410 only when you know it was removed on purpose and want to say so.

Writing them in Go

Go
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated) // before any body byte
json.NewEncoder(w).Encode(created)

Use the named constants and not bare numbers, so a reader does not need to remember that 422 is "Unprocessable". Call WriteHeader once, before writing the body; a second call is ignored. http.Error is for plain text, so for JSON set the headers yourself.

Go's names and the RFC's

The output shows Request Entity Too Large and Unprocessable Entity. RFC 9110 renamed these to "Content Too Large" and "Unprocessable Content", and Go keeps the older text for compatibility. The numbers are what matter on the wire. Also note that Go has no name for 499; nginx logs 499 when the client closes the connection before the response, so look for it in proxy logs and not in your handlers.

The authoritative list is the IANA HTTP status code registry, the semantics are in RFC 9110 section 15, and the constants are documented in the net/http package.

Also inWeb
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.