In a Go net/http middleware, how do you recognise a CORS preflight request and answer it?
answer
- two marks identify it, not one
- OPTIONS on its own is not enough
- look for Access-Control-Request-Method
- answer with 204 and no body
- return instead of calling next
basics
~20 sA CORS preflight is an OPTIONS request that also carries an Access-Control-Request-Method header. A Go middleware tests both, writes the Access-Control-Allow-Origin, -Methods and -Headers response headers, sends 204 via w.WriteHeader(http.StatusNoContent), and returns without calling next.ServeHTTP.
solid answer
~40 s`net/http` ships no CORS support, so you write the branch by hand. Inside a `func(http.Handler) http.Handler` wrapper I read `r.Header.Get("Origin")` and, if that origin is allowed, set `Access-Control-Allow-Origin` and add `Vary: Origin`. Then I test for a preflight: `r.Method == http.MethodOptions` **and** `r.Header.Get("Access-Control-Request-Method") != ""` — both, because a plain OPTIONS request is not necessarily a preflight. On that branch I set `Access-Control-Allow-Methods` and `Access-Control-Allow-Headers`, call `w.WriteHeader(http.StatusNoContent)` and `return`, so the request never reaches the `http.ServeMux` or the handler behind it. A preflight carries no body, no cookies and no application meaning, so there is nothing for the handler to do with it. Every other request falls through to `next.ServeHTTP(w, r)`.
code
go · 16 linesfunc withCORS(allowed map[string]bool, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" && allowed[origin] {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Add("Vary", "Origin")
}
if r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" {
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.WriteHeader(http.StatusNoContent)
return // the mux and the handler never see this request
}
next.ServeHTTP(w, r)
})
}go deeper
Be ready to name the two marks that identify a preflight and to write the handful of lines that answer it: set the Access-Control-Allow-* headers, call WriteHeader with 204, return.
Explain why the wrapper answers the preflight instead of passing it inward, and what a preflight does and does not carry — no body, no cookies, only the names of the headers the real request will send.
Show that you keep the short-circuit narrow so ordinary OPTIONS traffic is unaffected, and that you recognise a preflight failing because something answered it with a non-2xx before the CORS headers were written.
Own whether CORS is enforced in one wrapper inside the service or at the edge proxy in front of it, and be able to say what a split between the two costs the teams debugging a blocked browser call.
## What a preflight is, in terms of what arrives at your Go server Before a browser lets page JavaScript make certain cross-origin requests, it sends a separate, automatic request first and only issues the real one if the answer is satisfactory. That first request is the *preflight*. From the point of view of a Go server it is an ordinary inbound request with three distinguishing marks: - the method is `OPTIONS`; - there is an `Origin` request header naming the page's origin, e.g. `https://app.example.com`; - there is an `Access-Control-Request-Method` header naming the method the real request will use, and often an `Access-Control-Request-Headers` header listing the header names it will send. It has **no body**, **no cookies**, and none of the custom headers themselves — only their names. Your handler cannot authenticate it, log a user id for it, or read anything useful out of it. ## Why this is middleware code you write yourself The standard library has no CORS helper. `net/http` gives you the shape — a handler is anything with `ServeHTTP(http.ResponseWriter, *http.Request)`, and middleware is conventionally `func(http.Handler) http.Handler` — and you supply the policy. Because the preflight has no application meaning, it should be answered by the wrapper and never passed inward: the wrapper writes the response and *returns* instead of calling `next.ServeHTTP`. That is what "short-circuiting" means here, and it is the single most important structural fact about a CORS wrapper. ## The detection predicate The common mistake is to short-circuit on the method alone: ```go if r.Method == http.MethodOptions { /* answer 204 */ } ``` `OPTIONS` is a real method that clients send for other reasons, and a service may want to answer it itself. Testing both the method and the presence of `Access-Control-Request-Method` keeps the branch narrow: only a browser preflight sets that header, so only a browser preflight is swallowed. ```go isPreflight := r.Method == http.MethodOptions && r.Header.Get("Access-Control-Request-Method") != "" ``` `Header.Get` returns the empty string when the header is absent, so the `!= ""` test is the idiomatic presence check; there is no separate "has" method. ## What the preflight response must contain A preflight response is a status plus headers, with no body: - `Access-Control-Allow-Origin` — the allowed origin (see the sibling question on echoing it). Without this, nothing else matters. - `Access-Control-Allow-Methods` — the methods the real request may use. It is fine to answer with the whole set your API supports rather than reflecting the one method asked about. - `Access-Control-Allow-Headers` — the header names the real request may send, which must cover everything the browser listed in `Access-Control-Request-Headers`. - optionally `Access-Control-Allow-Credentials: true` when the real request will carry cookies, and `Access-Control-Max-Age` to let the browser reuse this answer for a while. Then `w.WriteHeader(http.StatusNoContent)` and `return`. 204 is the conventional status because there is no body; any 2xx works, and a non-2xx makes the browser treat the preflight as failed. This is why the wrapper must answer *before* anything that could reject the request on its own — a preflight that reaches an authentication check gets a 401, and the browser reports it to the frontend engineer as an opaque CORS error rather than as an auth problem. ## The non-preflight path Everything that is not a preflight — including the real request that follows it — falls through to `next.ServeHTTP(w, r)`. The browser re-checks `Access-Control-Allow-Origin` on *that* response too, so the origin headers belong outside the preflight branch, set on the way in, not only inside it. ## Mechanics worth knowing `w.Header()` returns the response header map; mutations to it only reach the wire if they happen **before** the first `WriteHeader` or `Write` call, because that is when `net/http` serialises the header block. Setting a CORS header after the inner handler has already written is a no-op. `Header.Set` replaces a value, `Header.Add` appends another — `Vary` is the header you normally `Add`, since another layer may already have contributed a value to it. ## Shape to remember Read the origin, set the origin headers, test for the preflight, answer 204 and return, otherwise call `next`. Four steps, and the ordering of the last two is where most hand-rolled CORS middleware goes wrong.
- Why test for Access-Control-Request-Method as well as the OPTIONS method?OPTIONS is a legitimate method a client may send for its own reasons, and only a browser preflight sets Access-Control-Request-Method. If the wrapper short-circuits every OPTIONS request, any non-preflight OPTIONS is swallowed with a 204 and never reaches the code that would have answered it. Testing both keeps the short-circuit narrow to the traffic that genuinely has no application meaning.
- Does a preflight carry the caller's cookies or Authorization header?No. The browser sends the preflight without credentials and without the custom headers themselves; it only names them in Access-Control-Request-Headers. That is why the CORS wrapper has to answer it before anything that inspects credentials — a preflight that reaches an authentication check is rejected, and the browser surfaces that to the frontend as a CORS failure rather than a 401.
- What status should the preflight response carry?Any 2xx; 204 No Content is conventional because the response has no body, and `w.WriteHeader(http.StatusNoContent)` says exactly that. What matters is that the status is successful and the Access-Control-Allow-* headers are present. A 4xx or 5xx — a 405 from routing, a 401 from an auth check, a 500 from a panic — makes the browser fail the preflight and never send the real request.
The preflight is the browser phoning ahead to ask whether the real call is welcome. The middleware answers the phone and hangs up; it does not put the caller through to the handler.
saying these in an interview costs you the question
- Treating every OPTIONS request as a CORS preflight
- Calling next.ServeHTTP after already answering the preflight
- Expecting net/http to handle CORS on its own
- Trying to read a body or cookies from a preflight
- Answering the preflight with 401 because no credentials arrived