slimphp / Slim-Http

A set of PSR-7 object decorators providing useful convenience methods

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Ensure headers are consistent

akrabat opened this issue · comments

  • Ensure that our headers implementation is completely compliant with PSR-7
  • Ensure that getHeader() and getHeaders() are consistent. See slimphp/Slim#2400

Before I start this PR, we need to agree on how the methods are going to behave.

Here is the PSR-7 HTTP Message Interfaces Spec

1.2 HTTP Headers
Case-insensitive header field names
HTTP messages include case-insensitive header field names. Headers are retrieved
by name from classes implementing the MessageInterface in a case-insensitive
manner. For example, retrieving the foo header will return the same result as
retrieving the FoO header. Similarly, setting the Foo header will overwrite
any previously set foo header value.

Despite that headers may be retrieved case-insensitively, the original case
MUST be preserved by the implementation, in particular when retrieved with
getHeaders().

Non-conforming HTTP applications may depend on a certain case, so it is useful
for a user to be able to dictate the case of the HTTP headers when creating a
request or response.

Retrieving headers is case-insensitive, one issue is they do not mention dashes and underscores.
The following two values pose a problem with the current implementation of Headers::normalizeKey() method:

  • HTTP-MY-HEADER
  • HTTP_MY_HEADER

If you do not explicitly set underscores_in_headers on;, nginx will silently drop HTTP headers with underscores (which are perfectly valid according to the HTTP standard as noted in RFC 7230 Section 3.2). This is done in order to prevent ambiguities when mapping headers to CGI variables, as both dashes and underscores are mapped to underscores during that process.

With that being said, the implementation of getHeaders() should preserve the original case but no mention of preserving the HTTP_ prefix is in the spec. If we look at a popular library like Guzzle PSR-7 they use this getallheaders() polyfill to retrieve all headers which does in fact remove the HTTP_ prefix and replaces underscores with dashes.

I would suggest the following:

  • We use getallheaders() always (not just if available, unlike current behavior) and make use of this getallheaders() polyfill. That means underscores will be replaced by dashes, and the HTTP_ prefix will be stripped. And we do that consistently and return them as they come in from the getallheaders() function.
  • We return the headers as they are originally processed just like Guzzle PSR-7 does in their MessageTrait. When the headers come in from getallheaders() they strtolower() all of them and in the getHeaders() function they return them without preserving orignal case despite what the PSR-7 spec says. This creates a consistent behavior.

Need feedback from everyone:
@akrabat, @designermonkey, @danopz, @llvdl, @jdrieghe

If I do $message = $message->withHeader('Content-Type', 'text/plain'); and then $message->getHeaders()., I'd expect the array key to be Content-Type, not content-type.

With that being said, the implementation of getHeaders() should preserve the original case but no mention of preserving the HTTP_ prefix is in the spec.

Please, don't keep the HTTP_ prefix. This would break one of my libraries that strongly depends on strict PSR-7 compliance. I actually got involved in the past to fix this issue in slim/http (slimphp/Slim#2091).

Yes, I agree completely with losing the HTTP prefix and the underscores when reading from the environment.

I am with @akrabat what you put in should be what you get out. Can't say i have ever seen headers with the underscore tho.

Okay so I guess someone has to make a choice here. Case sensitivity either matters across the board or it does not. That's the only way to achieve consistent output.

getHeader() is case insensitive. The input of withHeader() should not determine the output of getHeader() or getHeaders().

Consistent output should not depend on inconsistent input. That's just my 2 cents. @akrabat you can have the final say, I'm not sure what to do here.

commented

I would also prefer normalized header names, but apparently some HTTP clients do not treat header names as case insensitive and therefore PSR-7 dictates that getHeaders() returns the original case.

I suggest following PSR-7 and make a best effort with the request headers:

  • the header that is given with withHeader, getHeader and withAddedHeader should be normalized when comparing headers, but the original case should be stored for use with getHeaders.
  • use the getallheaders polyfill as suggested by @l0gicgate to get the original header names, which falls back to $_SERVER
  • if $_SERVER is used to fetch the headers, the HTTP_ prefix should be dropped as it is not part of the actual header name. Also underscores should be converted to hyphens.

I am unsure what to do with the normalization of underscores and hyphens in header names. It seems like hyphens are converted to underscores in $_SERVER. So the header name may then end up as my_header or my-header, depending on the web server used. It is probably an edge case, so I suggest not converting hyphens to underscores before comparing.

RFC2616 states that header names are HT (Hyphen Text) and so do no have underscores in them.

@llvdl's suggestion work for me. Holding the original text of the name along with the normalised version seems to be a solution.

@akrabat okay so getHeaders() will provide whatever the original output of the getallheaders() function returns? If so, that will all be ucword() case.

Define what you mean by normalized output and which method returns that.