Converting a FetchHttpClient response stream drops workerd's known-length capability
Los mantenedores suelen responder en 1 día
Ya se ha fusionado un pull request relacionado.
- #9673 de @effect-bot — fusionado
Evaluación
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Aptitud para principiantes
- 45/100
- Tipo de issue
- Error
- Claridad
- Bastante claro
- Estado de actividad
- Activo
- Stack tecnológico
- typescript
- Área
- backend
Línea de trabajo
Start by reproducing the failure with FetchHttpClient, HttpClientResponse.fromWeb(), Stream.fromReadableStream, and Stream.toReadableStreamEffect() in the issue's Vitest example. Inspect how the response body is wrapped and converted, then verify that direct conversion preserves the source body's known-length capability without changing arbitrary transformed streams. Done means the shown R2 write succeeds and the regression case passes.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
What version of Effect is running?
- Effect
4.0.0-rc.112 @cloudflare/vitest-pool-workers0.22.0- Miniflare
5.20260815.0-alpha/ workerd1.20260815.1
What steps can reproduce the bug?
import { env } from "cloudflare:workers"
import { Effect, Stream } from "effect"
import {
FetchHttpClient,
HttpClient,
HttpClientRequest
} from "effect/unstable/http"
import { expect, it, vi } from "vitest"
it("drops the native body's known length after Effect conversion", async () => {
const bytes = new Uint8Array([1, 2, 3])
const url = "https://example.com/archive.tgz"
vi.stubGlobal("fetch", () => Promise.resolve(new Response(bytes)))
const nativeResponse = await fetch(url)
if (nativeResponse.body === null) throw new Error("Missing response body")
await expect(
env.BACKUP.put("native.tgz", nativeResponse.body)
).resolves.not.toBeNull()
const program = Effect.gen(function*() {
const request = HttpClientRequest.get(url)
const response = yield* HttpClient.execute(request)
const body = yield* Stream.toReadableStreamEffect(response.stream)
yield* Effect.tryPromise(() => env.BACKUP.put("effect.tgz", body))
}).pipe(Effect.provide(FetchHttpClient.layer))
await expect(Effect.runPromise(program)).rejects.toThrow(
"Provided readable stream must have a known length"
)
})
The R2 operation rejects with:
Provided readable stream must have a known length
(request/response body or readable half of FixedLengthStream)
In the same runtime, the native response-body write succeeds. FixedLengthStream.readable also succeeds.
What is the expected behavior?
For the direct conversion shown above, Stream.toReadableStreamEffect(response.stream) should produce a Web stream that retains the source body's runtime-known length and is accepted by R2.
What do you see instead?
The public HttpClientResponse body API is an Effect Stream. Converting it with Stream.toReadableStreamEffect() creates a generic Web ReadableStream, which does not retain the runtime-internal identity-length capability that R2 queries.
The body bytes are unchanged, but R2 rejects the converted stream. This prevents direct streaming through HttpClientResponse.stream without reconstructing the length or buffering the body.
Additional information
HttpClientResponse.fromWeb() keeps the original Response in a private field. HttpClientResponse.stream wraps Response.body with Stream.fromReadableStream. The HTTP client also adds interruption and finalization wrappers. Stream.toReadableStreamEffect() then creates a new Web stream, so workerd can no longer query the source body's identity length.
Cloudflare documents ReadableStream as a valid R2Bucket.put() value, but workerd's R2 implementation requires a stream to report an identity length before it starts the write. This check is in workerd itself, not only in Miniflare.
This request concerns direct conversion of response.stream. An arbitrary transformed, filtered, concatenated, or generated Effect stream does not necessarily have a known length.
A caller can read Content-Length, create a Cloudflare FixedLengthStream, and pump the Effect stream into it with Sink.fromWritableStream. This is a platform-specific manual bridge, and it works only when the header is present and equals the number of bytes emitted by the stream. Buffering through response.arrayBuffer is another workaround when the body fits safely in memory.
Our workaround fetches archives with the platform fetch() and passes the original Response.body to R2. Effect still owns retries, errors, and timeouts around that operation. Packument requests continue to use FetchHttpClient.
- Lenguaje dominante
- TypeScript
- Estrellas
- 16.7k
- Forks
- 808
- Merge medio
- 10 h 33 min
- PR fusionados (30 d)
- 830
Preparar el entorno
Este proyecto no incluye contenedor de desarrollo, Dockerfile ni guía de contribución, así que la configuración corre por tu cuenta: empieza por su README y consulta nuestra guía para la primera contribución para los pasos generales.
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de Effect-TS/effect
-
RpcTest.makeClient: accept disableFatalDefectsPosiblemente ocupada @effect-bot la tomó hace 1 día. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
Effect-TS/effect#9893 · 3 comentarios ·
Los mantenedores suelen responder en 1 día
-
`HttpApiSchema.Upgrade`: allow a regular success for servers that don't upgradePosiblemente ocupada @effect-bot la tomó hoy. Abiertodocker-made-me-do-it enhancement
Effect-TS/effect#9912 · 1 asignado ·
Los mantenedores suelen responder en 1 día
-
docker-made-me-do-it enhancement
Dificultad 5/5 Más de una semana Aptitud para principiantes 20/100
Los mantenedores suelen responder en 1 día
-
`HttpApiSchema`: stream bodies in a custom framingPosiblemente ocupada @effect-bot la tomó hoy. Abiertodocker-made-me-do-it enhancement
Effect-TS/effect#9910 · 1 asignado ·
Los mantenedores suelen responder en 1 día
-
`HttpApiSchema` streams: declare every media type a stream can be sent asPosiblemente ocupada @effect-bot la tomó hoy. Abiertodocker-made-me-do-it enhancement
Effect-TS/effect#9909 · 1 asignado ·
Los mantenedores suelen responder en 1 día
Todos los issues de Effect-TS/effect
Issues similares
-
a10y a10y-audit ai-driven-qa claude
Dificultad 2/5 1-3 horas Aptitud para principiantes 86/100
linagora/twake-directory-manager#219 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
Los mantenedores suelen responder en 1 día
-
type: bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 79/100
Los mantenedores suelen responder en 1 día
-
0️⃣ phase/new
Dificultad 2/5 1-3 horas Aptitud para principiantes 82/100
Los mantenedores suelen responder en 1 día
-
Pulse v7: buildPreferencesFromTelos() bypasses telosSectionOrFile(), renders empty on unified-TELOS installsPosiblemente ocupada @zephyr91 la tomó hoy. Abierto
Dificultad 2/5 1-3 horas Aptitud para principiantes 75/100
danielmiessler/LifeOS#2393 ·
Los mantenedores suelen responder en 4 días