node icon indicating copy to clipboard operation
node copied to clipboard

http: add writeEarlyHints function to ServerResponse

Open wingleung opened this issue 1 year ago • 7 comments

Similar to the writeContinue and writeProcessing functions, we could introduce a writeEarlyHints function where we could write the early hints response in a slightly friendlier way

response._writeRaw('HTTP/1.1 103 Early Hints\r\nLink: <\/styles.css>; rel=preload; as=style\r\n\r\n', 'ascii')

would become

response.writeEarlyHints('</styles.css>; rel=preload; as=style')

// or in case of multiple links
response.writeEarlyHints(
  [
    '</globals.css>; rel=preload; as=style',
    '</styles.css>; rel=preload; as=style'
  ]
)

Problem: multiple headers

_writeRaw() cannot be used with multiple headers so I folded the headers into one line.

Link: </styles.css>; rel=preload; as=style, </scripts.js>; rel=preload; as=script

an alternative way to write this is the following

  this._writeRaw('HTTP/1.1 103 Early Hints\r\n', 'ascii');
  this._writeRaw('Link: </styles.css>; rel=preload; as=style\r\n', 'ascii');
  this._writeRaw('Link: </scripts.js>; rel=preload; as=script\r\n\r\n', 'ascii');

however, this also folds into a oneliner, so I think prefolding it before calling _writeRaw() just once was better.

The problem is these folded headers may work now but could become deprecated in future http releases. So maybe I should just write _writeRaw() multiple times and look into unfolding or prevent the folding of the sent header elsewhere 🤔

thoughts?

Resources

  • discussion about making folding obsolete in future http releases 👉 https://lists.w3.org/Archives/Public/ietf-http-wg/2015JulSep/0256.html
  • obs-fold 👉 https://www.ietf.org/rfc/rfc7230.html

wingleung avatar Aug 08 '22 18:08 wingleung

Review requested:

  • [ ] @nodejs/http
  • [ ] @nodejs/net

nodejs-github-bot avatar Aug 08 '22 18:08 nodejs-github-bot

CI: https://ci.nodejs.org/job/node-test-pull-request/45935/

nodejs-github-bot avatar Aug 08 '22 19:08 nodejs-github-bot

lgtm, solid work. It seems Chrome supports them now:

https://developer.chrome.com/blog/early-hints/

Could you add them to HTTP/2 too? Or it would be better to do it in a follow-up PR?

@mcollina good idea! I'll have to look into the http2 implementation first, I haven't read up on how the stream internals work exactly, first thought is to add it here but I'm not sure yet how it would work in a stream. Will have another look

wingleung avatar Aug 09 '22 11:08 wingleung

Also what should happen when neither a string nor array is passed in? Is a 103 without headers permitted? Should we throw an exception? Either way we should also add tests for other value types.

mscdex avatar Aug 09 '22 15:08 mscdex

Also what should happen when neither a string nor array is passed in? Is a 103 without headers permitted? Should we throw an exception? Either way we should also add tests for other value types.

@mscdex I added an else clause that will throw an ERR_INVALID_ARG_VALUE exception if it's not a string or an array. test case 👉 test/parallel/test-http-early-hints-invalid-argument.js

wdyt?

wingleung avatar Aug 09 '22 20:08 wingleung

What should happen if the array is empty? Could you add a test for this case?

@mcollina I think there will be scenarios where we want to create a links variable, do some manipulations on that based on the resource content and then pass it on to writeEarlyHints(), so if we would have content that results in an empty array then I think we can be graceful with it and just do nothing.

As opposed to invalid types like objects (what @mscdex mentioned ☝️ ), I think that's more of a programmatic error so in that case I'm throwing ERR_INVALID_ARG_VALUE

wdyt?

Added test cases

wingleung avatar Aug 09 '22 21:08 wingleung

minor nit: perhaps we could consolidate the tests into one file for this new feature?

Additionally we should test that the function throws when it should.

mscdex avatar Aug 09 '22 21:08 mscdex

minor nit: perhaps we could consolidate the tests into one file for this new feature?

Additionally we should test that the function throws when it should.

@mscdex because the invalid argument test throws an exception where I process.exit(0) afterwards, the other test might fail. I can work around it by adding a timeout but I think it's more scalable if we separate the throwable tests to their dedicated files 🤔 I kept the 3 happy flow test in 1 file though

wingleung avatar Aug 10 '22 12:08 wingleung

The first commit in PR should start with affected sybsystem, which is http (src mostly refers to generic changes in C++ parts of code). This can be fixed by rebasing and force-pushing, or on landing stage.

@LiviaMedeiros done, this is my first contrib to node so forgive me 😄

wingleung avatar Aug 10 '22 13:08 wingleung

CI: https://ci.nodejs.org/job/node-test-pull-request/45979/

nodejs-github-bot avatar Aug 10 '22 17:08 nodejs-github-bot

LGTM, thanks a lot for your contribution!

For reference, the contents of this Link header are described in RFC 8288, B.2. Parsing a Link Field Value in particular. AFAICT there is no separate validation algorithm; current validation is good enough but can be improved in future PRs.

@LiviaMedeiros thank you for mentioning that document, I realise there are more attributes possible and updated the regex accordingly 👉 https://github.com/nodejs/node/pull/44180/commits/35ea4b90bb32364d1287c45d53ccebd60b217961

only for the UTF8 type attribute I'm not sure we can support that?

also updated my regex test suite 👉 https://regexr.com/6rjub might be better to have some tests for all current validators

wingleung avatar Aug 11 '22 06:08 wingleung

CI: https://ci.nodejs.org/job/node-test-pull-request/46000/

nodejs-github-bot avatar Aug 11 '22 12:08 nodejs-github-bot

lgtm, solid work. It seems Chrome supports them now: https://developer.chrome.com/blog/early-hints/ Could you add them to HTTP/2 too? Or it would be better to do it in a follow-up PR?

@mcollina good idea! I'll have to look into the http2 implementation first, I haven't read up on how the stream internals work exactly, first thought is to add it here but I'm not sure yet how it would work in a stream. Will have another look

@mcollina as promised, I read up on the implementation of the http/2 protocol in node.js, I added the writeEarlyHints() function to the compatibility api where I just use the stream.additionalHeaders() function to add it to the stream 👉 lib/internal/http2/compat.js

wingleung avatar Aug 13 '22 15:08 wingleung

CI: https://ci.nodejs.org/job/node-test-pull-request/46040/

nodejs-github-bot avatar Aug 16 '22 08:08 nodejs-github-bot

lgtm

Amazing work!

I think you should add to the docs in https://nodejs.org/api/http2.html#class-http2http2serverresponse too

done! 👉 https://github.com/nodejs/node/pull/44180/files#diff-f63531002261f56a8c6723723d2b7e6d05ea9ec8d433195fdf7c47728f6e3700

wingleung avatar Aug 16 '22 08:08 wingleung

CI: https://ci.nodejs.org/job/node-test-pull-request/46043/

nodejs-github-bot avatar Aug 16 '22 09:08 nodejs-github-bot

Landed in 58ab0e28210b4107f50a419ae24c97b376d1ff18

nodejs-github-bot avatar Aug 17 '22 18:08 nodejs-github-bot

Why is the API restricted to only the Link header? The spec allows any header and I was about to use this for an intenral API to include some metadata and saw that it's only usable for Link. Would a PR be accepted to allow the argument to also be an object that is just a kvp of headers?

dougwilson avatar Aug 18 '22 15:08 dougwilson

@dougwilson Can you make an example?

mcollina avatar Aug 18 '22 15:08 mcollina

Sure. For example I'm thinking

response.writeEarlyHints({
  link: 'your link header',
  x-trace-id: 'id for diagnostics'
})

That would end up writing out

HTTP/1.1 103 Early Hints
link: your link header
x-trace-id: id for diagnostics

Spec: https://datatracker.ietf.org/doc/html/rfc8297

dougwilson avatar Aug 18 '22 15:08 dougwilson

Given this has not shipped in a release yet, we can still change it. I added a dont-land-on-v18.x for now.

The API you are mentioning makes total sense.

Do browsers (or API gateways) make any use of those other headers?

mcollina avatar Aug 18 '22 15:08 mcollina

AFAIK web browsers will only use the Link header (but I cannot say for sure as I am not an expert there), but what headers are in use by a client depend on the client. In our case, this is an internal API and we made our client use the trace header. This was helpful so we still have the trace header in the case the tcp connection dropped or the remote server had some other issue before sending the full response.

dougwilson avatar Aug 18 '22 15:08 dougwilson

Go ahead and send the PR!

mcollina avatar Aug 18 '22 16:08 mcollina

Sweet. I got it mostly do so far. I know it is new, so not sure on the process, but should this just be in addition to the current implementation or replace it? I.e. should I make it just take an object as the argument or should I keep this existing behavior intact and have it vary on the arg type (string = link, array = links, non array object = headers)?

dougwilson avatar Aug 18 '22 17:08 dougwilson

I'm not sure, let's keep everything for now and see in the PR.

mcollina avatar Aug 18 '22 17:08 mcollina

Cool, sounds good. That is what I was expecting, just it's a bit more work, so was trying to see if I could take a shortcut to open PR sooner, haha ;)

dougwilson avatar Aug 18 '22 17:08 dougwilson

If the spec allows any header I would make it simple and only use a kvp object. I think validateHeaderName() and validateHeaderValue() from _http_outgoing.js can be used to validate the key-value pairs.

lpinca avatar Aug 18 '22 18:08 lpinca

If the spec allows any header I would make it simple and only use a kvp object. I think validateHeaderName() and validateHeaderValue() from _http_outgoing.js can be used to validate the key-value pairs.

I agree ☝️, hard validation makes it more robust but if we have a more lenient approach everywhere else it makes sense to just have one object @dougwilson is proposing, good catch!

wingleung avatar Aug 18 '22 23:08 wingleung

Cool, sounds good. That is what I was expecting, just it's a bit more work, so was trying to see if I could take a shortcut to open PR sooner, haha ;)

Then take a shortcut and let's accept only an object!

mcollina avatar Aug 19 '22 09:08 mcollina

@dougwilson any updates on this one?

mcollina avatar Aug 25 '22 09:08 mcollina

Maybe a general api like response.writeInfomation(statusCode, [, statusMessage][, headers]) for send 1xx response. If have this api, no need more api for new 1xx status code.

killagu avatar Sep 08 '22 15:09 killagu