- From: Sam Gammon <notifications@github.com>
- Date: Mon, 19 Dec 2022 04:49:23 -0800
- To: whatwg/fetch <fetch@noreply.github.com>
- Cc: Subscribed <subscribed@noreply.github.com>
- Message-ID: <whatwg/fetch/issues/1575@github.com>
Hello esteemed WhatWG Fetch authors, I am a library author [helping to implement `fetch` support](https://github.com/axios/axios/pull/5146) downstream in the popular[^1] [Axios](https://github.com/axios/axios) library. For the uninitiated, Axios helps developers fetch data using various adapters (including XHR and Node's `http` module), and on various platforms (browsers and Node). Now that Fetch is very broadly supported[^2] (congrats, and thank you for all your hard work!), it is seeing new support in libraries such as Axios (hi! 👋🏻). Before proceeding, I just wanted to say that the Fetch API is one of the _smoothest_ and clearest API interfaces offered by the web, in my humble opinion; it is spreading because it is easy to use, refreshingly obvious in its behaviors and assumptions, and general enough to cover an extremely broad set of cases, from `await fetch('...')` to more complex scenarios with cancellation and streams. That being said, there is exactly _one_ place where frequent interaction with the Fetch API seems to fall short: **string representations for `Headers`, `Request`, and `Response`**. I'm writing today to see if the WhatWG can help clear this up across supporting implementations. ## Paragon case: `URL` When developing with `URL`, `Request`, and `Response` (referred to herein as _ancillary Fetch API objects_), the developer may often need to obtain a stringified representation, either for use elsewhere in their software, or for debugging purposes. `URL` objects are a bright spot of support for this: they are interchangeable with `string` objects for many intents and purposes within the realm of Fetch, and indeed in the browser in general: ```js const sample = new URL('https://github.com'); // you can create a URL from a string console.log(sample); // logs the entire tree of components the URL console.log(`${sample}`); // logs the URL as a string const string = sample.toString(); // obtains the URL in absolute string form ``` ``` const sample = new URL('https://github.com'); console.log(sample); console.log(`${sample}`); const string = sample.toString(); ``` <img width="742" alt="Screenshot 2022-12-19 at 3 52 04 AM" src="https://user-images.githubusercontent.com/171897/208419971-efb4c9e3-9b50-4393-b548-bbe3d19c83ca.png"> This behavior is consistent across all three major browser vendors: <img width="2315" alt="Screenshot 2022-12-19 at 3 49 39 AM" src="https://user-images.githubusercontent.com/171897/208419532-03e0fc89-4f98-4883-b05a-6d5c3bd14881.png"> > _From left to right: Firefox 107, Safari 16.2, Chrome 108_. Immediately, in the debug window, I can see the URL itself, and even browse the components of the URL. Fantastic! Server-side runtimes also nail this: <img width="732" alt="Screenshot 2022-12-19 at 3 56 29 AM" src="https://user-images.githubusercontent.com/171897/208420695-ff073b63-c410-4d23-9573-eb3e573183bd.png"> > _Node 18.x on the left, Deno 1.x on the right._ Engines are remarkably consistent about `URL`, even across platforms and runtimes. Arguably, `URL` used in this way is a light _value class_, i.e. just a strongly-typed and validated shell around the well-formed string. This case remains sadly isolated from other Fetch API objects, though. ## Problem case 1: `Headers` I first encountered problems with `Headers` when diagnosing issues in the early Fetch adapter code. Let's try this snippet: ```js const headers = new Headers({x: 1}); console.log(headers); console.log(`${headers}`); console.log(JSON.stringify(headers)); ``` What do we get? <img width="330" alt="Screenshot 2022-12-19 at 4 09 46 AM" src="https://user-images.githubusercontent.com/171897/208423034-587e144b-d25c-4ce8-9130-1b981c681014.png"> > _Depicted: Chrome._ ?? At first glance, the `Headers` object we just created looks empty. This is obviously not the case: the object has the header we put in it, it just isn't showing in the string representation. This can be confirmed with `headers.has("x")` or `headers.get("x")`, each of which return the expected value. Platforms and browsers are not as consistent on this point: Chrome is arguably the worst example, but everyone fails this test except Firefox, with Node/Deno getting by (barely): <img width="2097" alt="Screenshot 2022-12-19 at 4 15 37 AM" src="https://user-images.githubusercontent.com/171897/208424081-fcf46c7d-33aa-4ecf-a021-0f8c9da8d98d.png"> - **Firefox:** Shows the header when logged directly. Can't be stringified or JSONified. - **Safari:** Shows the methods when logged, not the header. Can't be stringified or JSONified. - **Chrome:** Shows nothing (`{}`) when logged (!!). Can't be stringified or JSONified. - **Node:** Shows a rather verbose message which includes the header. Can't be stringified or JSONified. - **Deno:** Roughly the same behavior as Node, but less verbose. Can't be stringified or JSONified. ## Problem case 2: `Request`/`Response` I won't bore you guys with the setup, let's take a look at how they behave across platforms: ```js const req = new Request('https://github.com'); console.log(req); console.log(`${req}`); console.log(JSON.stringify(req)); ``` <img width="675" alt="Screenshot 2022-12-19 at 4 37 13 AM" src="https://user-images.githubusercontent.com/171897/208427788-151d0472-585c-4867-81cb-61c83cc35eb9.png"> Lovely debugging experience logging it directly, and, while I understand binary exchange is the norm with HTTP/2 in broad adoption, whatever happened to `Request[GET https://github.com]`? The virtue of HTTP1.x was easy debuggability, and there's no reason we have to give that up in a post-binary world. <img width="2097" alt="Screenshot 2022-12-19 at 4 36 53 AM" src="https://user-images.githubusercontent.com/171897/208427729-bfcc5af2-9eb4-492a-9ca8-ff82e3ea3b86.png"> One more test, this time with `Response`: ```js const res = new Response('hello whatwg you guys rock'); console.log(res); console.log(`${res}`); console.log(JSON.stringify(res)); ``` <img width="668" alt="Screenshot 2022-12-19 at 4 40 03 AM" src="https://user-images.githubusercontent.com/171897/208428311-f73309ab-f43a-47ce-9469-a10b30754291.png"> No `HTTP 200 OK (51k bytes)`? Okay, okay. I've made my point. ## Why this is a problem If `URL` is a value class around a well-formed `URL` string (you can't create a `URL` object from an invalid URL string), one might assume that `Headers`, too, behaves as a value class around a well-formed `map<!string, string[]>`. This would make sense, especially since, as we all know, HTTP headers can be repeated, and so you always need a bit of ceremony around a regular `Map` to handle headers properly. This assumption, though, is broken in a number of ways: 1) The developer can't JSON-stringify the headers object, as one would expect to be the case with a regular object 2) Printing the object does not reliably show the contents the developer is most likely to be interested in, as one would expect with a regular object These are arguably cosmetic assumption breakages, but this problem gets a bit deeper. In order to diagnose or assert based on the full state of the `Headers` object, users are forced to iterate, carry + sync a second object, or defer use of `Headers` until the moment before a request is fired, which sadly neutralizes the benefits the API surface can provide (looking at you, repeated headers). Avoiding the `Headers` object is lame because it's so great. Iterating carries the risk of exploding input. Carrying a second object and keeping it in sync has many pitfalls. Thus, the developer experience is surprisingly hampered by this one API incongruity. ## Alternatives considered 1) **Additional observable API detail.** I understand this one, totally. Perhaps WhatWG doesn't want to mandate disclosure of these internal object details _unless and until_ the user explicitly accesses them: this allows implementors more latitude to privately cache or defer calculations with greater cover. - **Mitigation:** `Headers`, `Request`/`Response` metadata, all account for a very small footprint of data. Of course, I don't think the developer expects the entire response or request body to be printed to the terminal, but I would be very surprised if the internal structure of these objects isn't optimized for heavy read access already anyway as a matter of practicality (an assumption is made here that these objects are more likely to be read-from than written-to, by and large). 2) **Implementor freedom.** Perhaps consensus is too high a bar for this kind of functionality to be written directly into the specification, or perhaps implementors have voiced a desire to control this aspect of how the Fetch API behaves. - **Mitigation:** Perhaps WhatWG could consider mandating (or even recommending with a `SHOULD` or `MAY` clause) a minimum set of metadata for ancillary Fetch API objects. For example, the request method and URL is often sufficient to identify an in-flight request, and an HTTP status may be sufficient to identify the general disposition of a response. 3) **Maybe this has been discussed.** If so, my apologies, especially if a decision has been made on this topic. I was not able to find any related issues after a search on GitHub. Thank you in advance for consideration of this request. ✌🏻 [^1]: Publicly (on GitHub), 7.8 million projects use Axios. 103k of these are [other packages](https://github.com/novuhq/novu) which themselves constitute additional transitive addressable users. [^2]: CanIUse places `fetch` support [at 97.05%](https://caniuse.com/fetch) at the time of this writing. -- Reply to this email directly or view it on GitHub: https://github.com/whatwg/fetch/issues/1575 You are receiving this because you are subscribed to this thread. Message ID: <whatwg/fetch/issues/1575@github.com>
Received on Monday, 19 December 2022 12:49:37 UTC