Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FBCM-5157 Allow multiple SetCookie headers in response #197

Merged
merged 9 commits into from
Nov 17, 2022

Conversation

arthurxavierx
Copy link
Contributor

We add the HTTPure.MultiHeaders module and use it in HTTPure.Request and HTTPure.Response for reading and writing headers with multiple values.

We chose this approach instead of adding support for duplicated headers to HTTPure.Headers in order to avoid a large breaking change.

Resolves #157.

This module contains the `MultiHeaders` type which encapsulates the
concept of duplicated headers (or headers with multiple values, like
"Set-Cookie").
We add `multiHeaders` fields to both `HTTPure.Request.Request` and
`HTTPure.Response.Response`, so that headers with multiple values can be
both read from requests and written to responses.

We've chosen this incremental approach instead of changing
`HTTPure.Headers` to avoid a large breaking change.
@arthurxavierx arthurxavierx self-assigned this Nov 7, 2022
Copy link
Member

@davezuch davezuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks great! I just have a few questions/comments below.

test/Test/HTTPure/HeadersSpec.purs Show resolved Hide resolved
src/HTTPure/MultiHeaders.purs Outdated Show resolved Hide resolved
test/Test/HTTPure/MultiHeadersSpec.purs Show resolved Hide resolved
src/HTTPure/Response.purs Outdated Show resolved Hide resolved
Some people could have been relying on the old `show` functionality, so
we bring it back under the `toString` name.
Before this commit, headers that existed in both `headers` and
`multiHeaders` were written to the response only with their
`multiHeaders` values. We fix this so that they're joined as if they
were all added to `multiHeaders`.
Copy link
Member

@davezuch davezuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for making those changes! This looks great, but I do think we should add something to docs/Examples before merging.

When testing the old implementation of this function with an actual
node.js request, the `request.headersDistinct` property did not exist,
even in node.js versions greater than v16.17.0, which, according to
https://nodejs.org/api/http.html#messageheadersdistinct is when the
property was added.

We fix this by using `request.rawHeaders` instead.
@arthurxavierx
Copy link
Contributor Author

This looks great, but I do think we should add something to docs/Examples before merging.

Adding an example for MultiHeaders caught a bug: turns out that the request.headersDistinct-based implementation didn't work even with more recent node.js versions. I fixed it by using [request.rawHeaders] instead.

@davezuch
Copy link
Member

davezuch commented Nov 8, 2022

Adding an example for MultiHeaders caught a bug: turns out that the request.headersDistinct-based implementation didn't work even with more recent node.js versions. I fixed it by using [request.rawHeaders] instead.

Ah, nice catch! So it looks like the new implementation will have the desired behavior when there are duplicate Set-Cookie headers, but now I'm concerned about the handling for certain headers, and the asymmetry now between HTTPure.Headers and HTTPure.MultiHeaders. Specifically, request.headers—which HTTPure.Headers is implemented in terms of—lists these behaviors:

  • Duplicates of age, authorization, content-length, content-type, etag, expires, from, host, if-modified-since, if-unmodified-since, last-modified, location, max-forwards, proxy-authorization, referer, retry-after, server, or user-agent are discarded.

I think the behavior here makes sense. HTTPure.Headers discards duplicates, while HTTPure.MultiHeaders allows the user to decide what to do with them.

  • set-cookie is always an array. Duplicates are added to the array.

For HTTPure.MultiHeaders we now have the desired behavior, but with HTTPure.Headers we will have a runtime TypeError if we try to access this, since it will be an array, not a string. Perhaps in the FFI we should have a special case here that joins these into a string?

  • For duplicate cookie headers, the values are joined together with ; .

Seems like the HTTPure.Headers behavior is what we want, but what about HTTPure.MultiHeaders? Will these ever come as a single header string that we need to parse—splitting at ;? Even then, the user still has to parse key value pairs from the string. Would it make sense to go one step further and provide an HTTPure.Cookies module that gives access to a fully parsed Map of cookies?

  • For all other headers, the values are joined together with , .

I think the behavior here is fine on both ends.

@arthurxavierx
Copy link
Contributor Author

Even then, the user still has to parse key value pairs from the string. Would it make sense to go one step further and provide an HTTPure.Cookies module that gives access to a fully parsed Map of cookies?

I don't think that should be done in this PR.

For HTTPure.MultiHeaders we now have the desired behavior, but with HTTPure.Headers we will have a runtime TypeError if we try to access this, since it will be an array, not a string. Perhaps in the FFI we should have a special case here that joins these into a string?

We could just filter out set-cookies in HTTPure.Headers.read and specify in the docs that set-cookies headers should always be read from multiHeaders; what do you think?

@davezuch
Copy link
Member

davezuch commented Nov 9, 2022

Even then, the user still has to parse key value pairs from the string. Would it make sense to go one step further and provide an HTTPure.Cookies module that gives access to a fully parsed Map of cookies?

I don't think that should be done in this PR.

Agreed. Created an issue (#198) for the idea.

For HTTPure.MultiHeaders we now have the desired behavior, but with HTTPure.Headers we will have a runtime TypeError if we try to access this, since it will be an array, not a string. Perhaps in the FFI we should have a special case here that joins these into a string?

We could just filter out set-cookies in HTTPure.Headers.read and specify in the docs that set-cookies headers should always be read from multiHeaders; what do you think?

That works as well.

We must do this in order to avoid runtime type errors when trying to
access `Set-Cookie` request headers. The reason is that node.js
specializes the reading of `Set-Cookie` and makes that one specific
header an `Array String` instead of `String`.

See https://nodejs.org/api/http.html#messageheaders for more details.
@arthurxavierx
Copy link
Contributor Author

@davezuch done

Copy link
Member

@davezuch davezuch left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is great! I think it's good to go. I just want to point out the asymmetry of how the headers will be used on the request and response sides, to make sure we think it makes sense.

When creating a response, we're not going have duplicate headers in the HTTPure.Headers and HTTPure.MultiHeaders, we might construct something like:

response
  { headers =
      HTTPure.Headers.header "Host" "127.0.0.1:8000"
      <> HTTPure.Headers.header "Accept" "*/*"
  , multiHeaders =
      HTTPure.MultiHeaders.header "Cookie" "foo=bar"
      <> HTTPure.MultiHeaders.header "Cookie" "Authorization=\"Token XXX\""
  }

Meanwhile, when reading a request, we will have duplicate headers. The same headers in a request would look something like:

request
  { headers =
      HTTPure.Headers.header "Host" "127.0.0.1:8000"
      <> HTTPure.Headers.header "Accept" "*/*"
      <> HTTPure.Headers.header "Cookie" "foo=bar; Authorization=\"Token XXX\""
  , multiHeaders =
      HTTPure.MultiHeaders.header "Host" "127.0.0.1:8000"
      <> HTTPure.MultiHeaders.header "Accept" "*/*"
      <> HTTPure.MultiHeaders.header "Cookie" "foo=bar"
      <> HTTPure.MultiHeaders.header "Cookie" "Authorization=\"Token XXX\""
  }

That seem right?

@arthurxavierx
Copy link
Contributor Author

Yes, that seems about right to me. The reason is that we want to avoid breaking changes (except for the show one). If we only had MultiHeaders in Request, then users would have to consume headers as Array String with lookup, as opposed to the current String.

@davezuch
Copy link
Member

Yeah, I agree, I just wanted to make sure we're on the same page. As you mentioned, we will need to release this as a breaking change due to the change in behavior for show.

@arthurxavierx
Copy link
Contributor Author

arthurxavierx commented Nov 17, 2022

@davezuch Do you know what's the release process for this library? Looks like we should be running nix-shell --run generate-bower and nix-shell --run generate-docs before merging this?

@cprussin
Copy link
Collaborator

@cprussin
Copy link
Collaborator

Although it looks like some steps have changed now that the registry is a thing. Maybe see https://github.com/purescript-contrib/governance/blob/main/pursuit-preregistry.md and update the releasing guide accordingly? (probably just swap the link in step 4 to point to this page instead)?

@arthurxavierx
Copy link
Contributor Author

Oh I tried searching the repo for a file like that but missed it. Thanks @cprussin!

@arthurxavierx arthurxavierx merged commit e48c53c into main Nov 17, 2022
@arthurxavierx arthurxavierx deleted the FBCM-5157/arthur/multiple-SetCookie-headers branch November 17, 2022 19:02
@arthurxavierx arthurxavierx mentioned this pull request Nov 17, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Allow multiple SetCookie headers in response
3 participants