diff --git a/.changeset/tiny-files-flow.md b/.changeset/tiny-files-flow.md new file mode 100644 index 00000000000..a8f71a0d8a9 --- /dev/null +++ b/.changeset/tiny-files-flow.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +Fix partial file-backed HTTP bodies to report the selected byte range as their content length. diff --git a/packages/effect/src/unstable/http/HttpBody.ts b/packages/effect/src/unstable/http/HttpBody.ts index c43a60d4b38..1696d9c3276 100644 --- a/packages/effect/src/unstable/http/HttpBody.ts +++ b/packages/effect/src/unstable/http/HttpBody.ts @@ -484,12 +484,26 @@ export const stream = ( contentLength?: number ): Stream => new Stream(body, contentType ?? "application/octet-stream", contentLength) +const fileContentLength = ( + size: FileSystem.SizeInput, + options?: { + readonly bytesToRead?: FileSystem.SizeInput | undefined + readonly offset?: FileSystem.SizeInput | undefined + } +): number => { + const available = Math.max(0, Number(size) - Number(options?.offset ?? 0)) + return options?.bytesToRead === undefined + ? available + : Math.min(available, Math.max(0, Number(options.bytesToRead))) +} + /** * Creates a streaming HTTP body for a file path. * * **Details** * - * The effect requires `FileSystem`, stats the file to set the content length, and can fail with `PlatformError`. + * The effect requires `FileSystem`, stats the file to set the selected content length, and can fail with + * `PlatformError`. * * @category constructors * @since 4.0.0 @@ -510,7 +524,7 @@ export const file = ( stream( fs.stream(path, options), options?.contentType, - Number(info.size) + fileContentLength(info.size, options) )) ) @@ -519,7 +533,8 @@ export const file = ( * * **Details** * - * The effect requires `FileSystem`, uses the provided file size as the content length, and can fail with `PlatformError`. + * The effect requires `FileSystem`, uses the provided file size to determine the selected content length, and can + * fail with `PlatformError`. * * @category constructors * @since 4.0.0 @@ -540,6 +555,6 @@ export const fileFromInfo = ( stream( fs.stream(path, options), options?.contentType, - Number(info.size) + fileContentLength(info.size, options) ) ) diff --git a/packages/effect/test/unstable/http/HttpBody.test.ts b/packages/effect/test/unstable/http/HttpBody.test.ts new file mode 100644 index 00000000000..8429ebc356b --- /dev/null +++ b/packages/effect/test/unstable/http/HttpBody.test.ts @@ -0,0 +1,14 @@ +import { assert, it } from "@effect/vitest" +import { Effect, FileSystem, Stream } from "effect" +import { HttpBody } from "effect/unstable/http" + +it.effect("uses the selected byte count as partial file content length", () => + Effect.gen(function*() { + const body = yield* HttpBody.fileFromInfo("x", { size: 6n } as any, { offset: 2, bytesToRead: 2 }).pipe( + Effect.provideService(FileSystem.FileSystem, { + stream: () => Stream.succeed(new Uint8Array([3, 4])) + } as any) + ) + const bytes = yield* Stream.mkUint8Array(body.stream) + assert.strictEqual(body.contentLength, bytes.length) + }))