-
Notifications
You must be signed in to change notification settings - Fork 351
Trailer support in the API #981
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
Comments
As noted in #772 (comment) the current way it is exposed subsets what HTTP supports and therefore does not feel like a good starting point. Both for the internal and public API. Additionally, the public-facing API has not seen interest from implementers, at least for the past year and a half. Tests: web-platform-tests/wpt#20720. Closes #772. Follow-ups: #980 and #981.
We are in an unsatisfying situation. Using fetch API with chunked transfer encoding there is currently no way to get the time when the chunk reception has started, see whatwg/streams#1126 When i stumbled upon HTTP Trailer [1] i shortly believed that we could solve our issue with it. The idea is to send a list with timestamps as Trailer with information when each single chunk was put into "pipe" at server side. With some calculation we would be then enabled to calculate exact e2e network throughput. But unfortunately, fetch API doesn't support trailers. Is there any chance for Trailer support in fetch API in future? [1] https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer |
There's a chance, otherwise this would be closed. It does however require folks to be willing to implement it and drive the associated work that is required and that thus far has not materialized. |
Note that HTTP provides no guarantee that chunks are preserved end-to-end; furthermore, many (most?) HTTP APIs don't guarantee that chunk delineation is exposed accurately. |
I don't think that scenario is a good justification for adding trailers to the platform. It seems much better addressed by looking at timing and when bytes come out of the stream. Additionally, with the various layers in between JavaScript and HTTP-level chunking (TLS, QUIC, HTTP/2 and HTTP/3 frames, the browser's own mechanisms), guarantees about chunk boundaries wouldn't be coherent anyway. |
Yes that's true and exactly for this reason it is so incredibly difficult to calculate throughput on receiver side (in JavaScript app logic). And surprisingly accurate e2e chunk preservation is not needed here (and not what we are asking for), we just suggest that one should be able to add meta information at the end of running chunked transfer transmission as the HTTP spec allows. Please let me provide you a simple example for clarification why trailers would be helpful. On server we know the timestamp when the frame was sent. Regarding the comment by @davidben
this is the way we currently go and mostly fail to calculate throughput correctly. What you will get is more or less that network throughput equals the (current) streamed video bitrate. Please elaborate a bit more on this if we missed your point but pls think of the existence of the idle times -> chunks are NOT send immediately one after another, there are production/idle times between them. |
At 25 frames per second, you generate a single video frame at every 40 ms and assume you ship each frame as they are encoded. Large frames may take longer than 40 ms, smaller ones may take a ms or so to transmit depending on the server's network connection. But that is not that important. What is important is the interarrival time of these chunks as they are received by the client. Each chunk has two associated times (t1 and t2). We know the chunk's size and if we know delta t (t2-t1), we can compute the chunk's throughput. But, we know t2 only, not t1 - we can only estimate it [1]. If that estimate is not good, then the computed chunk throughput will be pretty wrong leading to incorrect bandwidth measurements. As a result, your adaptive streaming client will adapt incorrectly (mostly getting stuck at a bitrate lower than you could get otherwise). What @mlasak is asking for whether there is a way to expose t1 in the API. The info is in somewhere there, it just needs to be exposed. [1] Bandwidth Prediction in Low-Latency Chunked Streaming talk at https://mile-high.video/files/mhv2019/index.html |
FWIW S3 now providing checksums in HTTP trailers: https://docs.aws.amazon.com/AmazonS3/latest/userguide/checking-object-integrity.html This way you can upload and provide a checksum that is validated against before the upload is published. |
Howdy all, I work at Fastly on their JavaScript runtime and SDK 👋 What I am thinking as a potential solution is: Updating the [SameObject] Promise<Headers> trailers(); Updating the [HeadersInit](https://fetch.spec.whatwg.org/#typedefdef-headersinit) trailers; And the same for both Which in an application context would look like this: // Service-Worker API based example of reading a request trailer
// and responding with a response which contains the same trailer
addEventListener('fetch', event => event.respondWith(app(event)));
async function app(event) {
const request = event.request;
// Resolves when all trailer fields have been received and returns an instance of Headers
const incomingTrailers = await request.trailers();
const animal = incomingTrailers.get('animal');
const response = new Response('I am a body', {
headers: {
'Trailer': 'animal'
},
trailers: {
animal: animal
});
return response;
} |
I would definitely like to see the conversation around this progress but I'm not sure the proposal is adequate here. In some cases when sending trailers we do not actually want to calculate the value of a trailer until after the body payload has been fully sent (largely because we won't know what the full body is until it is fully streamed. In these cases, timing exactly when and how to get the content of the trailing header can be tricky. For instance, in the Workers runtime (https://github.com/cloudflare/workerd), in many cases, when streaming the body does not actually require javascript to be run, we will actually move the entire stream out of the runtime and stop executing javascript at all after the We use a similar optimization for certain Now, I'm not suggesting that this be designed around the specific optimizations of the workers runtime, just using those as an example. If the content of the trailing header cannot be calculated in advance and we have to provide a means of calculating it at the end of the processing of the payload then we could end up surfacing implementation and timing details under the covers that could end up very inconsistent from one runtime to the next, or could end up forcing such optimizations to be disabled entirely, which is a problem. |
I assume that awaiting this would force the entire body payload to be cached in memory? |
I would think the point of trailers is that they can be computed after streaming the response body, so specifying them to the response constructor wouldn't be what we want. If you know the trailer values at constructor time, you might as well make them headers instead. Also, on the receiving side, if I'm less concerned about the impact on deferred proxying in |
I'd propose it does not buffer, but instead consumes and discards the body if the body has not already been consumed. If wanting the body, then applications could read the body before reading the trailers. |
Maybe when calling the constructor, you specify a callback for
On the receiving end, invoking |
Keep in mind that these are reversed on client and server sides. On the client side, a On the receiving side, setting up an "on trailers" callback would avoid the issue of ordering when consuming the body. // client side fetch api
const resp = await fetch('http://example.org', {
headers: { 'trailers': 'foo' },
// Called when sending the headers...
trailers(headers) {
headers.set('foo', 'bar');
}
}
resp.ontrailers = (headers) => {
// Called when trailers are received.
}; // server side fetch api
export default {
async fetch(req) {
req.ontrailers = (headers) => {
// Called when trailers are received
};
// ...
return new Response(stream, {
headers: { 'trailers': 'foo' },
trailers(headers) {
headers.set('foo', 'bar');
}
});
}
} It feels rather clunky tho. |
Could deferred proxying take place if the Response object took a promise
for trailer headers rather than a callback? Not sure it’s actually worth it
as I suspect trailer cases require processing the entire body, but just
highlighting the possibility.
…On Wed, Jul 19, 2023 at 1:46 PM James M Snell ***@***.***> wrote:
Keep in mind that these are reversed on client and server sides.
On the client side, a trailers callback would need to be provided in the
RequestInit, while on the server-side, it needs to be on the ResponseInit.
On the receiving side, setting up an "on trailers" callback would avoid
the issue of ordering when consuming the body.
// client side fetch api
const resp = await fetch('http://example.org', {
headers: { 'trailers': 'foo' },
// Called when sending the headers...
trailers(headers) {
headers.set('foo', 'bar');
}
}
resp.ontrailers = (headers) => {
// Called when trailers are received.
};
// server side fetch api
export default {
async fetch(req) {
req.ontrailers = (headers) => {
// Called when trailers are received
};
// ...
return new Response(stream, {
headers: { 'trailers': 'foo' },
trailers(headers) {
headers.set('foo', 'bar');
}
});
}
}
—
Reply to this email directly, view it on GitHub
<#981 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AABRERYK57UVQYK4UWODBO3XRBBRJANCNFSM4JZOX5XQ>
.
You are receiving this because you commented.Message ID:
***@***.***>
|
Thoughts:
|
I think it is inherently impossible, yes -- unless the system buffers the body somewhere, but we don't want that, especially in edge proxies.
Would it make sense for this to be a property rather than a function? That provides nice symmetry between Request/Response and RequestInit/ResponseInit. Note that it's very common to use an instance of Request as a RequestInit, relying on the fact that all the same-named properties have the same type and semantics, so this might be more than a superficial difference. |
|
I've started to outline a proposal that covers both trailer support and early hints support here: https://docs.google.com/document/d/1P4MskkFd3HHFPGDr01O3wdFXukmX9i0jAb5uh9v9x8Q/edit ... comments welcome in the doc |
Just keeping the conversation going. Based on feedback on the doc I referenced above, I've iterated a bit more on an approach for trailers that should be workable. Specifically, to send trailers... we can add a new const resp = await connect('https://...', { method: 'POST', body: payload, trailers: Promise.resolve(trailingHeaders) }); When the underlying implementation has completely processed the payload and is ready to send trailing headers, it would await the To receive trailers, we would add a new resp.trailers.then((headers) => {
// `headers` is a `Headers` object
}); |
You mean that the sender would resolve the |
Not having consistent behavior between browsers there sounds like a recipe for incompatibilities. If support for trailers is added to the web platform, I think the fetch spec needs to be clear on how browsers should handle that, even if caching proxies can behave differently (which also sounds like a major problem to me - as do caching proxies that never had to support them before potentially ignoring trailers, though guess the increased prevalence of HTTPS should help mitigate that). The Chrome networking team has historically pushed back pretty strongly on adding trailers because it's a pretty massive change to network APIs - a ton of stuff interposes on network requests in browsers, and much of it would need to be updated (Edit: Also due to concerns about real world utility, and the expectation that the disk cache should respect cache-related trails). Has that changed? That obviously doesn't mean work here shouldn't proceed, but it's a consideration if the goal here is broad adoption. |
If I understand the comment correctly, yes, the change adopts your feedback. On the sending trailers side, the |
I definitely cannot speak for any of the browsers. What I do know is that for Node.js and Workers, we've had a number of requests to support trailers for a number of cases -- the most common requests are to support And yeah, I think the concerns around it being difficult to add trailers due to how it impacts the underlying implementation are absolutely valid. Supporting trailers in Workers is going to be a significant effort on multiple layers so I can definitely understand the reluctance. Still, it would be worthwhile, I think. |
Given the mutable nature of the Applications that insist on making a distinction may compare the headers before and after the body stream is consumed, or just assume that every header listed in the |
No, combining trailers with headers goes against the latest HTTP specification. |
Hey all, there's been some interest in supporting trailers in Deno for both the I believe the |
I'm definitely still very interested in moving things forward here. The approach outlined in the first half of https://docs.google.com/document/d/1P4MskkFd3HHFPGDr01O3wdFXukmX9i0jAb5uh9v9x8Q/edit#heading=h.tst1r01yr7a appears to make the most sense at the moment. For sending trailers, being able to specify a async function getTrailers() {
// await stuff
return new Headers([['a','b']]);
}
const req = new Request('https://example.org', { /** ... **/, trailers: getTrailers());
// likewise for `Response` For receiving trailers, something like...
|
Got here with the following situation: We have this long running job on the server triggered by a POST request. The job runs for minutes, and generates output bit by bit as it runs (that is; it reports its steps and progress indicators as it runs); the job not being my code I can't change it to make error messages follow a regular pattern. As the job never reads input, and because websocket security was designed wrong from the beginning, requiring much more code at the server endpoint to make it secure, a websocket is not appropriate. At the very end I can check the exit code of the process and report it back to the javascript caller; the natural implementation of this is a trailer. Searching for how to get trailers found an older version of the fetch() specification that had a trailers property; but it does not actually exist. The discussion points
So I actually think the original spec got it right; trailers is just another property of the same type as headers that returns undefined until the body is fully read, at which point it returns the collection. |
I still think this is something we should do as it's part of HTTP and with newer iterations of H/2 it's a feature that's a lot easier to make use of due to overall improved infrastructure.
My current thinking is that building this on top of #980 and #607 (as you can have multiple trailer header lists per request/response) is the way to go.
FetchObserver
could have asendTrailer(Headers trailer)
andontrailer
event handler or some such. Details probably best sorted once there's more firm implementer interest.The text was updated successfully, but these errors were encountered: