-
Notifications
You must be signed in to change notification settings - Fork 337
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
Proposed backward-incompatible change: requiring cloning all requests/responses #61
Comments
I just want to note there are alternatives here. For example, streams could track and expose a .drained state. This is a boolean. Its cheap. As far as I can tell it has no practical downsides. Its just not theoretically pure to the current vision of streams. Obviously @domenic and I disagree here. I just want to point it out as an option, though, before we decide to break backward compat. |
That's really overstating it. It mutates streams into a completely new data structure. You wouldn't ask someone to add a .drained to arrays, asking if they had started out nonempty but then someone .pop()ed them into emptiness. |
I guess I don't think that's a fair comparison. Streams and arrays are different. For example, can you close an array so data can't be put into it anymore? Do arrays restrict access to sequential value with only a single read each? These are features of streams that arrays do not have. And they are features which imply a drained state. Conceptually streams do have a drained state; closed and all possible data read. |
Of course, now that I think about it some more (sorry, just getting back from PTO so there are cobwebs) I don't think .drained as I defined it above completely solves the problem. What we really need to know is if the stream is closed, all possible data read, and at least one chunk has ever been read. That's a bit more unusual, I admit, but I still don't think it would be a problem to implement. We could also just expose it as an algorithm concept in streams so that fetch can use it but its not available to script. Or there is the wrapper alternative... (I don't recall why that was not adequate...) |
I feel like we already have a thread for debating the semantics of streams. I was hoping to use this one to solicit opinions on making the cloning versus not-cloning story sane. |
I'm not really opposed to this. Would also for allow other use-once objects to be added to Request/Response later on. I would like to rename the property if we do this to |
I'm happy to defer to @annevk here. |
What do you think @tyoshino? Is this a change Chrome is willing to make? |
Does it? https://fetch.spec.whatwg.org/#concept-http-network-or-cache-fetch step 1 seems to perform a clone, or am I misunderstanding? +@annevk |
This change would break sites that took code from http://jakearchibald.com/2014/offline-cookbook/#on-network-response - given we're starting to see high profile users of ServiceWorker (and therefore fetch & cache), I'm really nervious about this change unless it's backed up by usage stats. |
Wait, but isn't that code hilariously broken for non-empty-body requests anyway? |
That's true, but bodied requests will always fail going into the cache. They're GET only. |
I guess I don't see how anyone could seriously be using that cookbook code then on a real site, so it doesn't seem like a good argument. If anything I'm now more concerned about the footguns that current patterns allow. |
http://jakearchibald.com/2014/offline-cookbook/#putting-it-together does make it clear that you'd use a conbination of patterns depending on url & method, but I agree that the caching examples should |
Re @jakearchibald #61 (comment) https://fetch.spec.whatwg.org/#dom-global-fetch runs the Request ctor. Then, the step 31 of https://fetch.spec.whatwg.org/#dom-request sets |
It seems it's common pattern that we I'm personally fine with the change regarding the Fetch API in window (included from Chrome 42, last stable). But jakearchibald's view is more important as there're more users in SW. |
Sorry. We just recalled that the current Chrome (stable 43) "incorrectly" behaves as @domenic's idea. Chrome had a bug that it errors the second
In the Chromium bug entry @yutakahirano just posted, we've fixed it to conform to the current Fetch spec. So, till now, no one should be able to reuse a null-body Request for multiple |
Excellent, so let's unfix that as a start and then see about renaming the property. |
Yeah, it's accidentally great misbehavior. Regarding Response, Chrome 43 does allow calling |
My concerns are more around: var request = new Request('/');
fetch(request).then(response => {
caches.open('whatever').then(c => c.put(request, response));
}); …the above currently works in Chrome, are we suggesting it should fail? |
Yes we are. |
I'm really worried about breaking existing sites, especially if we're going in dataless. At the very least, we'll need to have We'll also need to keep |
Well per Chrome |
Right, but "Facebook goes down for all Chrome users due to 'service worker'" isn't something I want to read. |
Uh, wouldn't the clone fail as well if the used flag is set? |
Oh, sorry. I didn't check the sequence in #61 (comment). Right, |
Yes, but what would cause that to happen? |
If I'm understanding correctly:
Therefore a method that takes a request/response can be described as one of:
|
You're right. Its just .add()/addAll() that do that. |
Where does cache.put() require the body to be not-null? I've been accepting null body in our implementation. |
@jakearchibald that ignores streams. We need to figure out a model that makes sense with streams. I have a proposal. We keep the new In addition, to solve #55, we give In addition we rewrite everything in terms of a high-level "validate the Request/Response stream" algorithm that a) checks for the offset to be 0 b) checks for the obsolete flag to be unset which can be reused by Here are some examples to show what this means: req = new Request({body: ""})
assert(req.body !== null)
assert_false(req.bodyUsed)
fetch(req) // success
assert(req.body === null) // due to transfer
assert_true(req.bodyUsed) // due to obsolete flag
assert_reject(fetch(req)) // due to obsolete flag
req = new Request()
assert(req.body === null)
assert_false(req.bodyUsed)
fetch(req) // success, obsolete flag not set due to null body
assert(req.body === null)
assert_false(req.bodyUsed)
fetch(req) // success
req = new Request({body: "Hallo wereld!"})
assert(req.body !== null)
assert_false(req.bodyUsed)
fetch(req) // success
assert(req.body === null) // due to transfer
assert_true(req.bodyUsed) // due to obsolete flag
assert_reject(fetch(req)) // due to obsolete flag
req = new Request({body: "Hallo wereld!"})
req.body.getReader()
assert(req.body !== null)
assert_false(req.bodyUsed)
assert_reject(fetch(req)) // because lock not released
req = new Request({body: "Hallo wereld!"})
reader = req.body.getReader()
reader.releaseLock()
assert(req.body !== null)
assert_false(req.bodyUsed)
fetch(req) // success
req = new Request({body: "Hallo wereld!"})
reader = req.body.getReader()
reader.read().then(() => {
reader.releaseLock()
assert(req.body !== null)
assert_true(req.bodyUsed) // due to offset
assert_reject(fetch(req)) // due to offset
})
stream = new Pipe()
req = new Request({body: stream})
assert(req.body !== null)
assert_false(req.bodyUsed)
fetch(req) // success
assert(req.body === null) // due to transfer
assert_true(req.bodyUsed) // due to obsolete flag
assert_reject(fetch(req)) // due to obsolete flag So bodyUsed is defined as associated stream's offset being greater than 0 or obsolete flag being set. And we end up treating null differently from the empty string. I think this would be compatible with what we have today and allow for streams. |
hm, yeah, maybe it's ok to accept that. Makes sense for redirects. |
@annevk that all looks fine to me. Why does |
Will calling |
The design has evolved a bit. Remember that .text() in specific, and anything that reads from the stream in general, is going to use an exclusive reader. The whole point of the exclusive reader is to prevent others from viewing what's going on with the stream, since that would preclude optimizations like off-main-thread work (e.g. consider .json() doing off-main-thread JSON parsing). So, not really. You have to be the one reading to get progress, as always. Anything else means that we have to do synchronization back to the main thread and all that fun stuff. |
Er, I accidentally edited the punchline out of my last post before submitting: the |
Aha, that's the bit I was missing. Ta. |
|
@annevk why is it a 'transfer' rather than |
@jakearchibald I don't understand. |
@annevk the way I thought it worked was the new request's stream would be piping the stream of the request passed to its constructor. So: var r1 = new Request(url, { body: stream });
var r2 = new Request(r1); Both However, your model avoids the additional stream, which I guess is better as long as the transfer isn't confusing for developers. |
We could do that too I suppose. That would leave us with one less null to worry about. I guess implementations could still move the pointer and "lie" to JavaScript... Module that, does my proposal work? I would prefer it if everyone that reviewed it said so. |
in whatwg/streams#367 and linked comments, the plan was to compare offset at construction time with offset at fetch/cache.put/etc. time, not to check that it's always zero. But I guess that is the subject of whatwg/streams#367 (comment) so for "v1" we will check it's zero?
Yay, these are very helpful! This transfer semantic is pretty interesting and I guess does help solve the problem. Here is one more example that I believe follows from yours: req = new Request({body: someClosedReadableByteStream})
assert(req.body !== null)
assert_false(req.bodyUsed)
fetch(req) // success
assert(req.body === null) // due to transfer
assert_true(req.bodyUsed) // due to obsolete flag
assert_reject(fetch(req)) // due to obsolete flag i.e., this is equivalent to However, I am afraid of the issue @yutakahirano pointed out in yutakahirano/fetch-with-streams#37 (comment): req = new Request({ body: "Hello world!" });
req.body.cancel();
// offset is zero since nothing has been read, so
// now this is equivalent to the above case: req.body is just
// a closed, zero-offset RBS.
assert(req.body !== null);
assert_false(req.bodyUsed); // !!
fetch(req); // !! success
assert(req.body === null) // due to transfer
assert_true(req.bodyUsed) // due to obsolete flag
assert_reject(fetch(req)) // due to obsolete flag This situation seems fine to me, but I am afraid @wanderview will not like it, since author code managed to disturb the stream in some way and yet fetch still succeeded/bodyUsed is still false. |
Can we inspect whether a stream is canceled? I agree that scenario does not seem ideal. |
No, canceled just means closed because you were told nobody was interested so you closed up prematurely. |
(And yes, the 0 offset check could maybe be removed if we instead complicate the "is synthetic" check elsewhere.) So a non-read canceled stream is indistinguishable from an empty stream. I guess that's just how it is then... Not sure there's anything we could do about that if that's the semantics the Streams folks think are logical... |
(Note that I'm unclear what problem you think the transfer semantic is solving, as it has been there since the start. I have not touched that part of the Fetch specification for quite a while. I'm also happy to change it to something like @jakearchibald suggests so we can slowly move towards |
> [Fetch] Check / Set .bodyUsed in Request construction if the src body is null. > > We need to check / set .bodyUsed in RequestConstruction when a Request is given, > even if the Request has a null body. See [1] for details. > > 1: whatwg/fetch#61 > > BUG=501195 > > Review URL: https://codereview.chromium.org/1187653003 TBR=yhirano@chromium.org Review URL: https://codereview.chromium.org/1205053002 git-svn-id: svn://svn.chromium.org/blink/branches/chromium/2403@197739 bbb929c8-8fbe-4397-9dbb-9b2b20218538
|
Actually, we'll do that as a part of a new issue for fetch-with-streams. |
@annevk, I didn't realize that this issue should be addressed. Given that now bodyUsed relies on stream's disturbed property, having another flag sounds a little weird. What do you think about setting Request's body to a locked-closed-disturbed stream instead of null in the Request constructor? |
@yutakahirano could you open a new issue with the scenario that is not covered and your proposed solution? I think that would be clearer and make it easier for everyone to follow what is going on. Thank you. |
In yutakahirano/fetch-with-streams#37 @yutakahirano, @wanderview, @tyoshino and I have been going in circles with regard to a pretty big mismatch between how streams work and how Request/Response currently work. We have a proposal that would make everything a lot simpler, but it does require a backward-incompatible change. Hoping to get opinions on whether it's a feasible change. /cc @slightlyoff @jakearchibald
Basically, the premise is that it's extremely surprising to developers that
works, but
fails on the second fetch, since the body is drained. That is, the latter case requires cloning before use, whereas the former does not.
We propose that:
In my opinion this helps the story a lot. Right now the rules for when you need to clone are pretty darn arbitrary, and in fact can be a refactoring hazard, as shown above. It also solves a lot of headaches around streams + fetch integration---if you want to dive into the linked thread, you'll see that we're having a really hard time rationalizing bodyUsed and related behavior in terms of streams.
However, there might be people out there already using multi-fetch/cache on empty-body requests/responses. So this might be a non-starter. I suggested adding use counters, but that might take a while. What are others thoughts? I am hopeful that this is a rare pattern, but I might be wrong...
The text was updated successfully, but these errors were encountered: