1import { assert, describe, it } from "@effect/vitest"
2import * as ByteSize from "effect/ByteSize"
3import * as Effect from "effect/Effect"
4import * as FileSystem from "effect/FileSystem"
5import { HttpEffect, HttpPlatform, HttpServerRequest, HttpServerResponse, HttpStaticServer } from "effect/http"
6import * as Layer from "effect/Layer"
7import * as Option from "effect/Option"
8import * as Path from "effect/Path"
9import * as Stream from "effect/Stream"
10
11const services = Layer.mergeAll(
12 Path.layer,
13 FileSystem.layerNoop({
14 stat: () => Effect.succeed({ type: "File", size: ByteSize.bytes(10) } as FileSystem.File.Info)
15 }),
16 Layer.succeed(
17 HttpPlatform.HttpPlatform,
18 HttpPlatform.HttpPlatform.of({
19 platform: "web",
20 compression: { algorithms: new Set(), compressResponse: Effect.succeed },
21 fileResponse: () => Effect.succeed(HttpServerResponse.text("0123456789")),
22 fileWebResponse: () => Effect.die("unused")
23 })
24 )
25)
26
27describe("HttpStaticServer", () => {
28 it.effect("preserves the JavaScript MIME type through toWebHandler", () => {
29 const fileSystem = FileSystem.layerNoop({
30 stat: () =>
31 Effect.succeed({
32 type: "File",
33 size: ByteSize.zero,
34 mtime: Option.none()
35 } as FileSystem.File.Info),
36 stream: () => Stream.empty
37 })
38 const layer = Layer.mergeAll(
39 Path.layer,
40 fileSystem,
41 HttpPlatform.layer.pipe(Layer.provide(fileSystem))
42 )
43 return Effect.gen(function*() {
44 const app = yield* HttpStaticServer.make({ root: "/root" })
45 const response = yield* Effect.promise(() =>
46 HttpEffect.toWebHandler(app)(new Request("http://localhost/script.js"))
47 )
48 yield* Effect.promise(() => response.arrayBuffer())
49
50 assert.strictEqual(response.headers.get("content-type"), "text/javascript; charset=utf-8")
51 }).pipe(Effect.provide(layer))
52 })
53
54 it.effect("ignores Range on HEAD requests", () =>
55 Effect.gen(function*() {
56 const app = yield* HttpStaticServer.make({ root: "/root" })
57 const response = yield* Effect.promise(() =>
58 HttpEffect.toWebHandler(app)(
59 new Request("http://localhost/file.txt", {
60 method: "HEAD",
61 headers: { Range: "bytes=20-24" }
62 })
63 )
64 )
65
66 assert.strictEqual(response.status, 200)
67 assert.strictEqual(response.headers.get("content-length"), "10")
68 assert.strictEqual(response.headers.get("content-range"), null)
69 }).pipe(Effect.provide(services)))
70})
71
72type FileOptions = Parameters<HttpPlatform.HttpPlatform["Service"]["fileResponse"]>[1]
73
74const precisionServices = (size: bigint, calls: Array<{ path: string; options: FileOptions }>) =>
75 Layer.mergeAll(
76 Path.layer,
77 FileSystem.layerNoop({
78 stat: (path) =>
79 Effect.succeed({
80 type: path === "/root/directory" ? "Directory" : "File",
81 size: ByteSize.bytes(size)
82 } as FileSystem.File.Info)
83 }),
84 Layer.succeed(
85 HttpPlatform.HttpPlatform,
86 HttpPlatform.HttpPlatform.of({
87 platform: "web",
88 compression: { algorithms: new Set(), compressResponse: Effect.succeed },
89 fileResponse: (path, options) => {
90 calls.push({ path, options })
91 return Effect.succeed(HttpServerResponse.empty({ status: options?.status ?? 200 }))
92 },
93 fileWebResponse: () => Effect.die("unused")
94 })
95 )
96 )
97
98describe("HttpStaticServer precision", () => {
99 const size = 9007199254740993n
100 for (
101 const [url, range, start, end] of [
102 ["/file.bin", "bytes=0-0", 0n, 0n],
103 ["/file.bin", "bytes=-2", size - 2n, size - 1n],
104 ["/directory", "bytes=1-", 1n, size - 1n]
105 ] as const
106 ) {
107 it.effect(`preserves ${range} and the exact total for ${url}`, () => {
108 const calls: Array<{ path: string; options: FileOptions }> = []
109 return Effect.gen(function*() {
110 const app = yield* HttpStaticServer.make({ root: "/root" })
111 const response = yield* app.pipe(Effect.provideService(
112 HttpServerRequest.HttpServerRequest,
113 HttpServerRequest.fromWeb(new Request(`http://localhost${url}`, { headers: { Range: range } }))
114 ))
115 assert.strictEqual(response.status, 206)
116 assert.strictEqual(response.headers["content-range"], `bytes ${start}-${end}/${size}`)
117 assert.deepStrictEqual(calls, [{
118 path: url === "/directory" ? "/root/directory/index.html" : "/root/file.bin",
119 options: { status: 206, offset: start, bytesToRead: end - start + 1n }
120 }])
121 }).pipe(Effect.provide(precisionServices(size, calls)))
122 })
123 }
124
125 it.effect("preserves the exact total in a 416 response", () => {
126 const calls: Array<{ path: string; options: FileOptions }> = []
127 return Effect.gen(function*() {
128 const app = yield* HttpStaticServer.make({ root: "/root" })
129 const response = yield* app.pipe(Effect.provideService(
130 HttpServerRequest.HttpServerRequest,
131 HttpServerRequest.fromWeb(new Request("http://localhost/file.bin", { headers: { Range: "bytes=-0" } }))
132 ))
133 assert.strictEqual(response.status, 416)
134 assert.strictEqual(response.headers["content-range"], `bytes */${size}`)
135 assert.deepStrictEqual(calls, [])
136 }).pipe(Effect.provide(precisionServices(size, calls)))
137 })
138})