Navaneeth K N
Navaneeth K N

Reputation: 15501

How do I set HTTP status code conditionally in a Go HTTP server?

I have the following HTTP handler function:

func (h *UptimeHttpHandler) CreateSchedule(w http.ResponseWriter, r *http.Request) {
    defer r.Body.Close()
    dec := json.NewDecoder(r.Body)

    var req ScheduleRequest
    if err := dec.Decode(&req); err != nil {
        // error handling omited
    }

    result, err := saveToDb(req)
    if err != nil {
        // error handling omited
    }

    // Responding with 201-Created instead of default 200-Ok
    w.WriteHeader(http.StatusCreated)

    enc := json.NewEncoder(w)
    if err := enc.Encode(result); err != nil {
        // this will have no effect as the status is already set before
        w.WriteHeader(http.StatusInternalServerError)
        fmt.Fprintf(w, "%v", err)
    }
}

Above code does the following:

  1. Request comes as a JSON data. It is decoded into req
  2. Request is persisted in the DB. This returns a result object which will be the response

Now here, as soon as the DB insert is successful, we set the status code to 201. Then use a JSON encoder to stream the JSON encoded value directly to ResponseWriter.

When encode returns an error, I need to change the status code to 500. But currently I can't do it as Go allows to set the status code only once.

I can handle this by keeping the encoded JSON in memory, and setting the status code only when it is success. But then this creates unwanted copies and not really nice.

Is there a better way to handle this?

Upvotes: 3

Views: 4847

Answers (2)

dolmen
dolmen

Reputation: 8696

The JSON marshalling may fail if some of your types (deeply) can not be serialized to JSON.

So you should ask yourself: what are the case where the encoding of the response can fail?

If you know the answer, just fix those cases. If you don't know, this is probably that those cases just don't exist.

If you are not sure, the only way to catch errors is to buffer the encoding. You have to balance the "not nice" between buffering and not returning a clean error.

Anyway, returning a 500 error if just the JSON encoding failed is really not nice. At least, you should revert the object creation (the work of saveToDb), so you have to wrap in a transaction that you will rollback in any failure cases.

Upvotes: 0

Not_a_Golfer
Not_a_Golfer

Reputation: 49187

Expanding my comment a bit:

There is no way to do this without buffering. And if you think about it, even if there was, how would that be implemented? The response header needs to be sent before the contents. If the code depends on encoding the response, you must first encode, check the result, set the header and flush the encoded buffer.

So even if the Go API supported a "delayed" status, you would be basically pushing the buffering problem one level down, to the http library :)

You should make the call - either it's important to get the response code right at all costs and you can afford to buffer, or you want to stream the response and you cannot change the result.

Theoretically Go could create an encoding validator that will make sure the object you are trying to encode will 100% pass before actually encoding.

BTW, this makes me think of another thing - the semantic meaning of the response code. You return HTTP Created, right? It's the correct code since the object has in fact been created. But if you return a 500 error due to encoding, has the object not been created? It still has! So the Created code is still valid, the error is just in the encoding stage. So maybe a better design would be not to return the object as a response?

Upvotes: 2

Related Questions