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

fix(ext/http): ensure request body resource lives as long as response is alive #20206

Merged
merged 2 commits into from Aug 21, 2023

Conversation

mmastrac
Copy link
Member

@mmastrac mmastrac commented Aug 18, 2023

Deno.serve's fast streaming implementation was not keeping the request body resource ID alive. We were taking the Rc<Resource> from the resource table during the response, so a hairpin duplex response that fed back the request body would work.

However, if any JS code attempted to read from the request body (which requires the resource ID to be valid), the response would fail with a difficult-to-diagnose "EOF" error.

This was affecting more complex duplex uses of Deno.fetch (though as far as I can tell was unreported).

Simple test:

        const reader = request.body.getReader();
        return new Response(
          new ReadableStream({
            async pull(controller) {
              const { done, value } = await reader.read();
              if (done) {
                controller.close();
              } else {
                controller.enqueue(value);
              }
            },
          }),

And then attempt to use the stream in duplex mode:

async function testDuplex(
  reader: ReadableStreamDefaultReader<Uint8Array>,
  writable: WritableStreamDefaultWriter<Uint8Array>,
) {
  await writable.write(new Uint8Array([1]));
  const chunk1 = await reader.read();
  assert(!chunk1.done);
  assertEquals(chunk1.value, new Uint8Array([1]));
  await writable.write(new Uint8Array([2]));
  const chunk2 = await reader.read();
  assert(!chunk2.done);
  assertEquals(chunk2.value, new Uint8Array([2]));
  await writable.close();
  const chunk3 = await reader.read();
  assert(chunk3.done);
}

In older versions of Deno, this would just lock up. I believe after 23ff0e7, it started throwing a more explicit error:

httpServerStreamDuplexJavascript => ./cli/tests/unit/serve_test.ts:1339:6
error: TypeError: request or response body error: error reading a body from connection: Connection reset by peer (os error 54)
    at async Object.pull (ext:deno_web/06_streams.js:810:27)

@@ -721,7 +721,7 @@ function createStreamTest(count: number, delay: number, action: string) {
}

for (const count of [0, 1, 2, 3]) {
for (const delay of [0, 1, 1000]) {
for (const delay of [0, 1, 25]) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer need a 1000ms test here -- there are no more timeouts in Deno.serve that exercise this path, so we can just use 25ms and make tests faster.

@@ -133,10 +133,6 @@ class InnerRequest {
}

close() {
if (this.#streamRid !== undefined) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stream closing now happens in Rust.

Copy link
Member

@bartlomieju bartlomieju left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

ext/http/http_next.rs Show resolved Hide resolved
ext/http/response_body.rs Outdated Show resolved Hide resolved
@mmastrac mmastrac merged commit 576d0db into denoland:main Aug 21, 2023
13 checks passed
littledivy pushed a commit to littledivy/deno that referenced this pull request Aug 21, 2023
… is alive (denoland#20206)

Deno.serve's fast streaming implementation was not keeping the request
body resource ID alive. We were taking the `Rc<Resource>` from the
resource table during the response, so a hairpin duplex response that
fed back the request body would work.

However, if any JS code attempted to read from the request body (which
requires the resource ID to be valid), the response would fail with a
difficult-to-diagnose "EOF" error.

This was affecting more complex duplex uses of `Deno.fetch` (though as
far as I can tell was unreported).

Simple test:

```ts
        const reader = request.body.getReader();
        return new Response(
          new ReadableStream({
            async pull(controller) {
              const { done, value } = await reader.read();
              if (done) {
                controller.close();
              } else {
                controller.enqueue(value);
              }
            },
          }),
```

And then attempt to use the stream in duplex mode:

```ts

async function testDuplex(
  reader: ReadableStreamDefaultReader<Uint8Array>,
  writable: WritableStreamDefaultWriter<Uint8Array>,
) {
  await writable.write(new Uint8Array([1]));
  const chunk1 = await reader.read();
  assert(!chunk1.done);
  assertEquals(chunk1.value, new Uint8Array([1]));
  await writable.write(new Uint8Array([2]));
  const chunk2 = await reader.read();
  assert(!chunk2.done);
  assertEquals(chunk2.value, new Uint8Array([2]));
  await writable.close();
  const chunk3 = await reader.read();
  assert(chunk3.done);
}
```

In older versions of Deno, this would just lock up. I believe after
23ff0e7, it started throwing a more
explicit error:

```
httpServerStreamDuplexJavascript => ./cli/tests/unit/serve_test.ts:1339:6
error: TypeError: request or response body error: error reading a body from connection: Connection reset by peer (os error 54)
    at async Object.pull (ext:deno_web/06_streams.js:810:27)
```
littledivy pushed a commit that referenced this pull request Aug 21, 2023
… is alive (#20206)

Deno.serve's fast streaming implementation was not keeping the request
body resource ID alive. We were taking the `Rc<Resource>` from the
resource table during the response, so a hairpin duplex response that
fed back the request body would work.

However, if any JS code attempted to read from the request body (which
requires the resource ID to be valid), the response would fail with a
difficult-to-diagnose "EOF" error.

This was affecting more complex duplex uses of `Deno.fetch` (though as
far as I can tell was unreported).

Simple test:

```ts
        const reader = request.body.getReader();
        return new Response(
          new ReadableStream({
            async pull(controller) {
              const { done, value } = await reader.read();
              if (done) {
                controller.close();
              } else {
                controller.enqueue(value);
              }
            },
          }),
```

And then attempt to use the stream in duplex mode:

```ts

async function testDuplex(
  reader: ReadableStreamDefaultReader<Uint8Array>,
  writable: WritableStreamDefaultWriter<Uint8Array>,
) {
  await writable.write(new Uint8Array([1]));
  const chunk1 = await reader.read();
  assert(!chunk1.done);
  assertEquals(chunk1.value, new Uint8Array([1]));
  await writable.write(new Uint8Array([2]));
  const chunk2 = await reader.read();
  assert(!chunk2.done);
  assertEquals(chunk2.value, new Uint8Array([2]));
  await writable.close();
  const chunk3 = await reader.read();
  assert(chunk3.done);
}
```

In older versions of Deno, this would just lock up. I believe after
23ff0e7, it started throwing a more
explicit error:

```
httpServerStreamDuplexJavascript => ./cli/tests/unit/serve_test.ts:1339:6
error: TypeError: request or response body error: error reading a body from connection: Connection reset by peer (os error 54)
    at async Object.pull (ext:deno_web/06_streams.js:810:27)
```
@mcandeia
Copy link

Hello guys, I am facing a problem that only happens on deno 1.36.2, I was taking a look in the repository trying to figure which could be the change that potentially caused this and due to my lack of knowledge in rust I selected this one cause the title is close enough to what I've done to fix the issue so far.

The problem is that the transfer-encoding was changed to chunked (previously no transfer-encoding was set by deno) when a new Response(body, {headers, status}) is used using a previously created response as a parameter. Looks like our front-end framework (fresh, currently) is not handling well the idea of receiving this data chunked.

Check my PR changes to make it work if it could help: https://github.com/deco-cx/deco/pull/392/files

Tested with + deno 1.36.1

Deno.serve((_req) => {
  const resp = new Response("hello, world");
  return new Response(resp.body);
});

Then

curl http://localhost:8000 -vvv
image

Now with deno 1.36.2 (used deno upgrade 1.36.2)

image

cc: @mmastrac @bartlomieju

@mcandeia
Copy link

tbh can be this one too: https://github.com/denoland/deno/pull/20057/files

@mmastrac
Copy link
Member Author

mmastrac commented Aug 23, 2023

I think it might be from #20180

@mcandeia Can you explain what's going on in Fresh? Chunked encoding vs content-length is an internal detail that we only offer a best effort, unless you specifically pass a buffer or string.

What is specifically failing in fresh?

@mcandeia
Copy link

What is specifically failing in fresh?

Looks like islands larger than size X are broken, partially streamed to the browser. same for large HTMLs. I'm trying to reproduce it in a simple example than I can post it here.

cc: @marvinhagemeister

@mcandeia
Copy link

Let's move the discussion to the right issue then? I can reproduce this using fresh getting started, I'm going to create a sample repo

@mcandeia
Copy link

Issue created on fresh: #20252

@mmastrac
Copy link
Member Author

@mcandeia Thanks for the repro! I'll look at this ASAP.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants