Skip to content

Request Duplex (Readable Stream) Support - #3190

Open
mookums wants to merge 2 commits into
mainfrom
request-open-stream
Open

mookums wants to merge 2 commits into
mainfrom
request-open-stream

Conversation

@mookums

@mookums mookums commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

This adds support for Request taking a ReadableStream.

https://developer.mozilla.org/en-US/docs/Web/API/Request/duplex

@krichprollsch

Copy link
Copy Markdown
Member

@mookums the PR is in draft, is it ready for review?

@mookums

mookums commented Aug 18, 2026

Copy link
Copy Markdown
Contributor Author

Was mostly finished but needed some updating on some tests.

@mookums
mookums force-pushed the request-open-stream branch 2 times, most recently from 3b214ff to 8b7b46b Compare August 28, 2026 15:07
@krichprollsch

Copy link
Copy Markdown
Member

@mookums are you still working on this one?

@mookums
mookums force-pushed the request-open-stream branch from b2b13e0 to 5ab8c12 Compare September 2, 2026 14:11
@mookums

mookums commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Yeah, done now. It supports taking in a ReadableStream for the Request body, consuming it before use and throwing an error if it isn't ready. It prevents the error on #3163 that is present on main.

@mookums
mookums marked this pull request as ready for review September 2, 2026 14:16

@krichprollsch krichprollsch left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

One suggestion to resolve the sharing stream issue could be:

  // Resolve a stream body to bytes on first need, so every copy (clone(), or
  // `new Request(other)`) owns its own bytes. Null means no body.
  fn resolveBody(self: *Request) !?[]const u8 {
      switch (self._body) {
          .none => return null,
          .bytes => |b| return b,
          .stream => |s| {
              const collected = s.collectBodyBytes(self._arena.allocator()) catch return error.TypeError;
              self._body = .{ .bytes = collected };
              return collected;
          },
      }
  }

Then:

  pub fn clone(self: *Request, exec: *const Execution) !*Request {
      // Fetch: clone() throws if the body is already disturbed.
      if (self._body_used) return error.TypeError;

      const body = try self.resolveBody();
      const arena = try exec.getPinnedArena(if (body) |b| b.len else 512, "Request.clone");
      errdefer arena.release();
      ...
      ._body = if (body) |b| .{ .bytes = try arena.dupe(u8, b) } else .none,

and

  .request => |r| blk: {
      if (r._body_used) return error.TypeError;
      break :blk if (try r.resolveBody()) |b| .{ .bytes = try arena.dupe(u8, b) } else .none;
  },

Or we need Tee for request clone

fn consume(self: *Request, local: *const js.Local) ?js.Promise {
if (self._body == null) {
return null;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Shouldn't we keep this early return by testing the case of body == .none?


pub fn clone(self: *const Request, exec: *const Execution) !*Request {
const arena = try exec.getPinnedArena(if (self._body) |b| b.len else 512, "Request.clone");
const arena = try exec.getPinnedArena(512, "Request.clone");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Keep the hint when body is bytes.

  const arena = try exec.getPinnedArena(switch (self._body) {
      .bytes => |b| b.len,
      .none, .stream => 512,
  }, "Request.clone");

}

const body_bytes: []const u8 = switch (request._body) {
.stream => |stream| stream.collectBodyBytes(request._arena.allocator()) catch {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

You drain the stream, but you let the request in an invalid state.
you should replace the request's body with the collected bytes.
as you do in Request.consume

switch (self) {
.none => return .none,
.bytes => |b| return .{ .bytes = try allocator.dupe(u8, b) },
.stream => |s| return .{ .stream = s },

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not sure it's safe to share the stream. If one collect it, the other won't be updated 🤔

.none => .none,
.bytes => |b| .{ .bytes = try arena.dupe(u8, b) },
.stream => |s| blk: {
break :blk .{ .stream = s };

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

same about sharing the stream

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.

2 participants