Skip to content

net/http: delete inappropriate headers in func Error #66343

Closed
@rsc

Description

@rsc

#50905 reported a bug about ServerContent serving bad headers when the request range is invalid. An example usage is:

w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Length", strconv.Itoa(len(jsonBody)))
w.Header().Set("Etag", etag)
http.ServeContent(w, req, "", time.Time{}, bytes.NewReader(jsonBody))

In this case, if http.ServeContent calls http.Error, it already forces Content-Type back to text/plain, but it leaves the Content-Length and Etag headers in place. This ends up being wrong for the error, though they would have been correct for a successful response.

CL 554216 added a deletion of Content-Length in http.Error. That's obviously correct, since Error is writing the content: the caller cannot predict how large it is. There was no problem with that CL.

CL 544109 did more: it deletes Content-Encoding, Etag, and Last-Modified, and it forces Cache-Control to no-cache.

Deleting Content-Encoding is correct for the same reasons as Content-Length.

I am not convinced that deleting Etag and Last-Modified is correct. Those describe the resource at the URL, and it seems sensible to me to say that a response can be an error that says “something about your request was invalid” but still also send back information about the underlying resource. It's not obviously wrong in the way that using the underlying resource's Content-Encoding or Content-Length is obviously wrong.

I am also not convinced that forcing Cache-Control to no-cache is correct. Error results can absolutely be cached. https://cloud.google.com/media-cdn/docs/caching, for example, describes how Google's CDN handles cached errors. An argument might be made that in situations like http.ServeContent, the caller can only set Cache-Control for the successful response, and so Error should assume that's not the expiry for an error. Perhaps that is true.

CL 544109 forced Cache-Control: no-cache instead of deleting the header. That broke many tests inside and outside Google.

CL 569815 changed the logic to only force Cache-Control to no-cache when it was already set to something else. This still breaks some tests inside Google; I am not sure about outside. It still seems incorrect to me. If we believe that the content being returned by Error is not accurately described by w.Header() on entry in certain ways, then why should these two code snippets produce different headers?

w.Header().Set("Cache-Control", "public, max-age=86400")
http.ServeFile(w, r, "/tmp/x.txt")

versus

http.ServeFile(w, r, "/tmp/x.txt")

I cannot justify why one error should have “Cache-Control: no-cache” while the other should have no header at all. Always or never is easier to justify than “depends on a header we already established shouldn't apply to the error response”.

I rolled back CL 544109 and CL 569815 because of the Cache-Control breakage, and I am making this a proposal because of the compatibility implications (we've already had many broken tests).

It seems plenty defensible to me to change Error to do:

  • Delete Content-Length (already done, obviously inappropriate)
  • Delete Content-Encoding
  • Delete Etag
  • Delete Last-Modified

I propose we do those. I don't expect any objections here but won't be surprised if I am missing something.

For Cache-Control specifically, it seems like there are four options:

  1. Leave Cache-Control unmodified (historical Go behavior)
  2. Force Cache-Control to no-cache unconditionally (CL 544109)
  3. Replace extant Cache-Control with no-cache but leave unset unset (CL 569815)
  4. Delete Cache-Control unconditionally.

I propose we do (4), because:

  • We have already identified problems with (1), (2), and (3).
  • (4) aligns with the other deletions we already do.
  • (4) matches (1) for the many Go programs in the world that never think about Cache-Control at all, so it creates the least amount of churn (tied with (3), but (3) has the contradiction noted above).

If people are in favor of that proposal, then the next question is whether to gate these with a GODEBUG setting. The first two in the list seem like obvious bug fixes, because they are about the actual content form, and that's the job of func Error; the caller does not control that content. The last two are less defensible, because they are about the semantics of the response, and the caller may well want to be describing the error by setting those headers. For example consider code that tries to create a cacheable error today:

w.Header().Set("Cache-Control", "public, max-age=3600")
http.Error(w, "you're a teapot", 410)

Perhaps we should have a GODEBUG that controls the last three deletions?
Let's say httpcleanerrorheaders=0 preserves Etag, Last-Modified, and Cache-Control.

Then the complete proposal is

  • Delete Content-Length (already done, obviously correct)
  • Delete Content-Encoding (obviously correct)
  • Delete Etag
  • Delete Last-Modified
  • Delete Cache-Control
  • GODEBUG=httpcleanerrorheaders=0 disables the Etag, Last-Modified, and Cache-Control deletions

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    Status

    Done

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions