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

Vendor our own fetch polyfill #529

Closed
saghul opened this issue May 31, 2024 · 17 comments
Closed

Vendor our own fetch polyfill #529

saghul opened this issue May 31, 2024 · 17 comments
Labels
enhancement New feature or request web-api

Comments

@saghul
Copy link
Owner

saghul commented May 31, 2024

The one we use now has some problems:

  • Lack of streaming support (and the maintainer does not want to add it)
  • Shenanigans as we polyfill it, since it detects support for things at import time

Generally speaking, it's small enough that it makes sense to incorporate it and make our own changes to it.

@saghul saghul added enhancement New feature or request web-api labels May 31, 2024
@Tobbe
Copy link

Tobbe commented Jun 3, 2024

  • Lack of streaming support (and the maintainer does not want to add it)

Am I right in assuming this is what you're referring to? JakeChampion/fetch#198 (comment)

Are there any other polyfills out there that could be a good fit for txiki.js? Or did you want to vendor JakeChampion/fetch and just add/modify what you need?

@saghul
Copy link
Owner Author

saghul commented Jun 3, 2024

Am I right in assuming this is what you're referring to? JakeChampion/fetch#198 (comment)

Yes, pretty much.

Are there any other polyfills out there that could be a good fit for txiki.js? Or did you want to vendor JakeChampion/fetch and just add/modify what you need?

The latter, I think. I realized it's small enough that it should be easy to vendor and adapt, like remove all the support checks wich are done only once, since we know what the runtime supports.

@saghul
Copy link
Owner Author

saghul commented Jun 19, 2024

Actually I've started to think that a better idea would be to drop XHR completely and implement fetch straight on top of libcurl.

If Node, Deno and Bun can get away with not having XHR we can too, I guess.

@lal12
Copy link
Contributor

lal12 commented Jun 19, 2024

Actually I've started to think that a better idea would be to drop XHR completely and implement fetch straight on top of libcurl.

If Node, Deno and Bun can get away with not having XHR we can too, I guess.

Though there are some features not available in standard browser fetch that are possible with XHR. So some extensions are neeeded (nodejs e.g. has some too).

@saghul
Copy link
Owner Author

saghul commented Jun 19, 2024

Do you have examples of those?

@lal12
Copy link
Contributor

lal12 commented Jun 19, 2024

Two specifics I though of:

  • Sending streams as body (available in chrome recently, not in FF or Safari)
    • also needed for progress of uploading (though some callback for this instead might also be useful)
  • Controlling https policy to allow untrusted certificates e.g. (not available in browser at all)

I think being able to get the TCP connection from the fetch api (e.g. via Connection: Keep-alive or transport upgrade) to further use it would also be useful. Though this might raise other issues/questions when it is a SSL connection.

@saghul
Copy link
Owner Author

saghul commented Jun 19, 2024

Good points!

I don't think we can extract the connection easily, since we don't use our own TCP socket, curl does...

@lal12
Copy link
Contributor

lal12 commented Jun 19, 2024

Good points!

I don't think we can extract the connection easily, since we don't use our own TCP socket, curl does...

It's possible to either use CURLOPT_WRITEFUNCTION / CURLOPT_OPENSOCKETFUNCTION to supply your own socket. But that has issues for SSL. Though it also seems to be possible to get the socket from curl, and prevent it from closing (CURLOPT_CLOSESOCKETFUNCTION). Though I'm not sure how both would integrate with SSL.

@guest271314
Copy link
Contributor

Make sure duplex: "half" and HTTP/2 are supported so we can full duplex stream.

@saghul
Copy link
Owner Author

saghul commented Jul 24, 2024

Where can I find docs about the duplex option? I couldn't find anything on mdn.

@guest271314
Copy link
Contributor

"half" is the only valid value and it is for initiating a half-duplex fetch (i.e., the user agent sends the entire request before processing the response). "full" is reserved for future use, for initiating a full-duplex fetch (i.e., the user agent can process the response before sending the entire request). This member needs to be set when body is a ReadableStream object. See issue #1254 for defining "full".

Example client, in code, from oven-sh/bun#7206

full_duplex_fetch_test.js
full_duplex_fetch_test.js.zip

var wait = async (ms) => new Promise((r) => setTimeout(r, ms));
var encoder = new TextEncoder();
var decoder = new TextDecoder();
var { writable, readable } = new TransformStream();
var abortable = new AbortController();
var { signal } = abortable;
var writer = writable.getWriter();
var settings = { url: "https://comfortable-deer-52.deno.dev", method: "post" };
fetch(settings.url, {
  duplex: "half",
  method: settings.method,
  // Bun does not implement TextEncoderStream, TextDecoderStream
  body: readable.pipeThrough(
    new TransformStream({
      transform(value, c) {
        c.enqueue(encoder.encode(value));
      },
    }),
  ),
  signal,
})
  // .then((r) => r.body.pipeThrough(new TextDecoderStream()))
  .then((r) =>
    r.body.pipeTo(
      new WritableStream({
        async start() {
          this.now = performance.now();
          console.log(this.now);
          return;
        },
        async write(value) {
          console.log(decoder.decode(value));
        },
        async close() {
          console.log("Stream closed");
        },
        async abort(reason) {
          const now = ((performance.now() - this.now) / 1000) / 60;
          console.log({ reason });
        },
      }),
    )
  ).catch(async (e) => {
    console.log(e);
  });
await wait(1000);
await writer.write("test");
await wait(1500);
await writer.write("test, again");
await writer.close();

Deno server code on Deno Deploy that echoes to the Native Messaging extension, and public requests, i.e., from /to the client above

Deno Deploy full-duplex server

const responseInit = {
  headers: {
    'Cache-Control': 'no-cache',
    'Content-Type': 'text/plain; charset=UTF-8',
    'Cross-Origin-Opener-Policy': 'unsafe-none',
    'Cross-Origin-Embedder-Policy': 'unsafe-none',
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Private-Network': 'true',
    'Access-Control-Allow-Headers': 'Access-Control-Request-Private-Network',
    'Access-Control-Allow-Methods': 'OPTIONS,POST,GET,HEAD,QUERY',
  },
};

for await (
  const conn of Deno.listen({
    alpnProtocols: ["h2", "http/1.1"],
  })
) {
  for await (const {
      request,
      respondWith
    }
    of Deno.serveHttp(conn)) {
    if (request.method === 'OPTIONS' || request.method === 'HEAD') {
      respondWith(new Response(null, responseInit));
    }

    if (request.method === 'GET') {
      respondWith(new Response(null, responseInit));
    }
    try {
      const stream = request.body
        .pipeThrough(new TextDecoderStream())
        .pipeThrough(
          new TransformStream({
            transform(value, c) {
              c.enqueue(value.toUpperCase());
            },
            async flush() {

            },
          })
        ).pipeThrough(new TextEncoderStream());
      respondWith(new Response(
        stream, responseInit));
    } catch (e) {

    }
  }
}

The above is a single HTTP/2 connection. The closest we get to that in HTTP 1.1 is WebSocketStream. The excpetion, as noted above, is between a ServiceWorker and a WindowClient on Chromium-based browsers. I've asked. Not sure how Chromium authors implement that particular connection.

Expected result

$ deno run -A full_duplex_fetch_test.js
495.081126
TEST
TEST, AGAIN
Stream closed
$ node --experimental-default-type=module full_duplex_fetch_test.js
1286.0589990019798
TEST
TEST, AGAIN
Stream closed

What happens in Bun, that does not, yet, implement HTTP/2

$ bun run full_duplex_fetch_test.js
24112.282304
Stream closed

@saghul
Copy link
Owner Author

saghul commented Jul 25, 2024

Thanks!

@saghul
Copy link
Owner Author

saghul commented Sep 3, 2024

The fetch polyfill is vendored as of #647

I laid out the plan for the future in that PR.

I'm closing this one since the initial goal (simple vendoring of the polyfill) is done, but will be coming back for the useful comments.

@saghul saghul closed this as completed Sep 3, 2024
@guest271314
Copy link
Contributor

tjs run full-duplex-fetch-test.js
Error: Unsupported payload type
    at send (polyfills.js:2:37741)
    at <anonymous> (polyfills.js:2:3115)
    at Promise (native)
    at k (polyfills.js:2:3115)
    at <anonymous> (full-duplex-fetch-test.js:21:1)
    at evalFile (native)
    at <anonymous> (run-main.js:27:1435)

Maybe try to use Workerd, Deno, or Node.js implementation of WHATWG Fetch and WHATWG Streams?

@saghul
Copy link
Owner Author

saghul commented Sep 6, 2024

The change needs to happen in the native implementation now, and I cannot borrow that.

@guest271314
Copy link
Contributor

Doesn't txiki.js depend on cURL? cURL supports HTTP/2.

@saghul
Copy link
Owner Author

saghul commented Sep 6, 2024

Yes, but the code needs to be written to send data to the native side in chunks in case it's a ReadableStream.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request web-api
Projects
None yet
Development

No branches or pull requests

4 participants