@statewalker/webrun-http-proxy 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -19,15 +19,26 @@ function urlUpstream(init) {
19
19
  if (init.via != null) headers.set("via", init.via);
20
20
  for (const [name, value] of Object.entries(init.headers ?? {})) headers.set(name, value);
21
21
  for (const [name, value] of Object.entries(init.credential?.() ?? {})) headers.set(name, value);
22
- const outbound = new Request(target, {
23
- method: request.method,
24
- headers,
25
- body: request.body,
26
- ...request.body != null ? { duplex: "half" } : {},
27
- signal: request.signal,
28
- redirect: "manual"
29
- });
22
+ const bodyless = request.method === "GET" || request.method === "HEAD";
23
+ let body;
24
+ let streaming = false;
25
+ if (!bodyless && request.body != null) {
26
+ body = request.body;
27
+ streaming = true;
28
+ }
30
29
  try {
30
+ if (!bodyless && !streaming) {
31
+ const buffered = await request.arrayBuffer();
32
+ if (buffered.byteLength > 0) body = buffered;
33
+ }
34
+ const outbound = new Request(target, {
35
+ method: request.method,
36
+ headers,
37
+ body,
38
+ ...streaming ? { duplex: "half" } : {},
39
+ signal: request.signal,
40
+ redirect: "manual"
41
+ });
31
42
  const upstream = await doFetch(outbound);
32
43
  if (upstream.type === "opaqueredirect" || upstream.status === 0) return new Response("upstream redirected; the proxy does not follow redirects", {
33
44
  status: 502,
@@ -1 +1 @@
1
- {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEnE,0HAA0H;AAC1H,eAAO,MAAM,MAAM,mBAAmB,CAAC;AAEvC;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,YAAY,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAExC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,+FAA+F;IAC/F,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACtD,qDAAqD;IACrD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,QAAQ,CA+D3D"}
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,OAAO,KAAK,OAAO,CAAC,QAAQ,CAAC,CAAC;AAEnE,0HAA0H;AAC1H,eAAO,MAAM,MAAM,mBAAmB,CAAC;AAEvC;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,YAAY,CAAC;AAEpC,MAAM,WAAW,eAAe;IAC9B;;;;;;;OAOG;IACH,mBAAmB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IAExC,0EAA0E;IAC1E,IAAI,EAAE,MAAM,CAAC;IACb,uEAAuE;IACvE,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,+FAA+F;IAC/F,UAAU,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IACtD,qDAAqD;IACrD,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;IACzB,+CAA+C;IAC/C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;;;GAMG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,eAAe,GAAG,QAAQ,CAkH3D"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-http-proxy",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "type": "module",
5
5
  "description": "Re-issue an HTTP request to an outside origin, safely: credentials consumed, hop-by-hop headers dropped, body streamed.",
6
6
  "license": "MIT",
@@ -13,6 +13,7 @@
13
13
  "types": "./dist/index.d.ts",
14
14
  "exports": {
15
15
  ".": {
16
+ "source": "./src/index.ts",
16
17
  "types": "./dist/index.d.ts",
17
18
  "import": "./dist/index.js"
18
19
  }
@@ -24,13 +25,13 @@
24
25
  "sideEffects": false,
25
26
  "dependencies": {},
26
27
  "devDependencies": {
27
- "@biomejs/biome": "^2.5.8",
28
- "@types/node": "^26.2.0",
29
- "hono": "^4.13.2",
28
+ "@biomejs/biome": "^2.5.15",
29
+ "@types/node": "^26.6.4",
30
+ "hono": "^4.13.12",
30
31
  "rimraf": "^6.1.3",
31
- "rolldown": "^1.2.4",
32
+ "rolldown": "^1.2.12",
32
33
  "typescript": "^7.0.2",
33
- "vitest": "^4.1.10"
34
+ "vitest": "^5.0.3"
34
35
  },
35
36
  "publishConfig": {
36
37
  "access": "public"
package/src/proxy.ts CHANGED
@@ -99,16 +99,67 @@ export function urlUpstream(init: UrlUpstreamInit): Upstream {
99
99
  for (const [name, value] of Object.entries(init.headers ?? {})) headers.set(name, value);
100
100
  for (const [name, value] of Object.entries(init.credential?.() ?? {})) headers.set(name, value);
101
101
 
102
- const outbound = new Request(target, {
103
- method: request.method,
104
- headers,
105
- body: request.body,
106
- ...(request.body != null ? { duplex: "half" as const } : {}),
107
- signal: request.signal,
108
- redirect: "manual",
109
- });
102
+ // FIREFOX HAS NO `Request.prototype.body` (checked against 155):
103
+ // `request.body` reads as `undefined` there even for a genuine payload, so
104
+ // `body: request.body` silently builds a bodyless outbound request in that
105
+ // engine while working in Chromium, where the property is a stream. This
106
+ // defect has shipped from this exact pattern four times already
107
+ // (statewalker/httpeers: edge-dispatch.ts, two sites in core/router.ts, a
108
+ // demo page) — a POST through a session origin arrived as 0 bytes sent, in
109
+ // Firefox only, everywhere else silent.
110
+ //
111
+ // GET/HEAD is decided FIRST and never touches the body at all -- the
112
+ // Fetch spec forbids giving either one a body (`new Request(url, {
113
+ // method: "GET", body })` throws), so deciding this before anything else
114
+ // removes the await for the common case and removes any reliance on a
115
+ // byte count being the only thing standing between a GET and that throw.
116
+ // Matches `bodyOf` in httpeers-core, which the other four fixed sites use.
117
+ const bodyless = request.method === "GET" || request.method === "HEAD";
118
+
119
+ // `duplex` is a plain boolean decided in the branch that already knows,
120
+ // not `body instanceof ReadableStream` checked later: a stream built in a
121
+ // different realm (crossing a worker or iframe boundary) fails that
122
+ // `instanceof`, `duplex` would then be silently omitted, and `new
123
+ // Request` throws "duplex option is required" for a stream body.
124
+ let body: ReadableStream<Uint8Array> | ArrayBuffer | undefined;
125
+ let streaming = false;
126
+ if (!bodyless && request.body != null) {
127
+ // The streaming path stays first and unconditional: a runtime that has
128
+ // request streams never reaches the buffering fallback below, and
129
+ // never buffers an upload.
130
+ body = request.body;
131
+ streaming = true;
132
+ }
110
133
 
111
134
  try {
135
+ if (!bodyless && !streaming) {
136
+ // Firefox. Without `request.body` there is nothing to stream from, so
137
+ // the only way to see the payload at all is to buffer the whole of
138
+ // it. This runs INSIDE the try: Firefox hides `body` for EVERY
139
+ // request, so a request whose body was already consumed elsewhere
140
+ // throws `TypeError: Body is unusable` here too, and uncaught that
141
+ // would turn a proxied call into a rejected promise instead of this
142
+ // package's own `502 upstream-unreachable` -- a new failure mode, in
143
+ // exactly the engine this buffering exists to fix. Caught here, it is
144
+ // reported the same as any other unreachable upstream.
145
+ //
146
+ // A zero-length result is indistinguishable from an absent body
147
+ // (Chromium's own `new Request(url, { method: "POST", body: "" })`
148
+ // yields a non-null *empty* stream, handled by the branch above), so
149
+ // an empty buffer here is treated as absent too.
150
+ const buffered = await request.arrayBuffer();
151
+ if (buffered.byteLength > 0) body = buffered;
152
+ }
153
+
154
+ const outbound = new Request(target, {
155
+ method: request.method,
156
+ headers,
157
+ body,
158
+ ...(streaming ? { duplex: "half" as const } : {}),
159
+ signal: request.signal,
160
+ redirect: "manual",
161
+ });
162
+
112
163
  const upstream = await doFetch(outbound);
113
164
  // AN OPAQUE REDIRECT HAS STATUS 0, and `new Response(body, {status: 0})`
114
165
  // throws — which is how the shipping proxy reports a redirecting