Compare commits

...
Author SHA1 Message Date
Kit Langton 3913203f0f feat(server): stream fs.read responses with HTTP Range support 2026-10-03 22:17:16 -07:00
9 changed files with 475 additions and 46 deletions

No files matched your search

+32 -27
View File
@@ -2,7 +2,7 @@ export * as FileSystem from "./filesystem.js"
import { makeLocationNode } from "@opencode/util/effect/app-node"
import path from "path"
import { Context, Effect, Layer, Schema } from "effect"
import { Context, Effect, Layer, type Option, type PlatformError, Schema, type Stream } from "effect"
import { FSUtil } from "@opencode/util/fs-util"
import { Location } from "./location.js"
import { AbsolutePath, PositiveInt, RelativePath } from "./schema.js"
@@ -87,10 +87,19 @@ export class GrepInput extends Schema.Class<GrepInput>("FileSystem.GrepInput")({
export const Event = FileSystem.Event
export interface File {
readonly path: AbsolutePath
readonly mime: string
readonly size: number
readonly mtime: Option.Option<Date>
readonly stream: (options?: {
readonly offset?: number
readonly bytesToRead?: number
}) => Stream.Stream<Uint8Array, PlatformError.PlatformError>
}
export interface Interface {
readonly read: (
input: ReadInput,
) => Effect.Effect<{ readonly content: Uint8Array; readonly mime: string }, NotFoundError>
readonly read: (input: ReadInput) => Effect.Effect<File, NotFoundError>
readonly list: (input?: ListInput) => Effect.Effect<Entry[]>
readonly find: (input: FindInput) => Effect.Effect<Entry[]>
/** Writes a file at an absolute path or one relative to the location; not confined to it. */
@@ -129,25 +138,25 @@ const baseLayer = Layer.effect(
return Effect.die(cause)
}),
)
const resolve = Effect.fnUntraced(function* (input?: RelativePath) {
const absolute = path.resolve(location.directory, input ?? ".")
if (!FSUtil.contains(location.directory, absolute))
return yield* Effect.die(new Error("Path escapes the location"))
const real = yield* fs.realPath(absolute)
if (!FSUtil.contains(root, real)) return yield* Effect.die(new Error("Path escapes the location"))
const resolve = Effect.fnUntraced(function* (input: RelativePath) {
if (input.includes("\u0000")) return yield* Effect.fail(new NotFoundError({ path: input }))
const absolute = path.resolve(location.directory, input)
if (!FSUtil.contains(location.directory, absolute)) return yield* Effect.fail(new NotFoundError({ path: input }))
const real = yield* fs.realPath(absolute).pipe(
Effect.catchReason(
"PlatformError",
"NotFound",
() => Effect.fail(new NotFoundError({ path: input })),
(_, error) => Effect.die(error),
),
)
if (!FSUtil.contains(root, real)) return yield* Effect.fail(new NotFoundError({ path: input }))
return { absolute, real, directory: location.directory }
})
return Service.of({
find: search.find,
read: Effect.fn("FileSystem.read")(function* (input) {
const target = yield* resolve(input.path).pipe(
Effect.catchReason(
"PlatformError",
"NotFound",
() => Effect.fail(new NotFoundError({ path: input.path })),
(_, error) => Effect.die(error),
),
)
const target = yield* resolve(input.path)
const info = yield* fs.stat(target.real).pipe(
Effect.catchReason(
"PlatformError",
@@ -156,17 +165,13 @@ const baseLayer = Layer.effect(
(_, error) => Effect.die(error),
),
)
if (info.type !== "File") return yield* Effect.die(new Error("Path is not a file"))
if (info.type !== "File") return yield* Effect.fail(new NotFoundError({ path: input.path }))
return {
content: yield* fs.readFile(target.real).pipe(
Effect.catchReason(
"PlatformError",
"NotFound",
() => Effect.fail(new NotFoundError({ path: input.path })),
(_, error) => Effect.die(error),
),
),
path: AbsolutePath.make(target.real),
mime: FSUtil.mimeType(target.real),
size: Number(info.size),
mtime: info.mtime,
stream: (options) => fs.stream(target.real, options),
}
}),
list: Effect.fn("FileSystem.list")(function* (input = {}) {
+11 -5
View File
@@ -1,7 +1,7 @@
import fs from "fs/promises"
import path from "path"
import { describe, expect } from "bun:test"
import { Cause, Effect, Exit, Layer, PlatformError } from "effect"
import { Cause, Effect, Exit, Layer, Option, PlatformError, Stream } from "effect"
import { FSUtil } from "@opencode/util/fs-util"
import { LayerNode } from "@opencode/util/effect/layer-node"
import { FileSystem } from "@opencode/core/filesystem"
@@ -37,13 +37,18 @@ describe("FileSystem", () => {
withTmp((directory) =>
Effect.gen(function* () {
yield* Effect.promise(() => fs.writeFile(path.join(directory, "text.txt"), "hello"))
yield* Effect.promise(() => fs.writeFile(path.join(directory, "data.bin"), Buffer.from([0, 1, 2])))
yield* Effect.promise(() => fs.writeFile(path.join(directory, "data.bin"), Buffer.from([0, 1, 2, 3, 4])))
const service = yield* FileSystem.Service
const text = yield* service.read({ path: RelativePath.make("text.txt") })
const binary = yield* service.read({ path: RelativePath.make("data.bin") })
expect(new TextDecoder().decode(text.content)).toBe("hello")
expect(new TextDecoder().decode(yield* Stream.mkUint8Array(text.stream()))).toBe("hello")
expect(text.mime).toBe("text/plain")
expect(binary.content).toEqual(new Uint8Array([0, 1, 2]))
expect(text.size).toBe(5)
expect(Option.isSome(text.mtime)).toBe(true)
expect(yield* Stream.mkUint8Array(binary.stream())).toEqual(new Uint8Array([0, 1, 2, 3, 4]))
expect(yield* Stream.mkUint8Array(binary.stream({ offset: 1, bytesToRead: 3 }))).toEqual(
new Uint8Array([1, 2, 3]),
)
}).pipe(provide(directory)),
),
)
@@ -190,9 +195,10 @@ describe("FileSystem", () => {
// the location root to the real directory.
const read = yield* FileSystem.Service.pipe(
Effect.flatMap((service) => service.read({ path: RelativePath.make("file.txt") })),
Effect.flatMap((file) => Stream.mkUint8Array(file.stream())),
provide(link),
)
expect(new TextDecoder().decode(read.content)).toBe("linked")
expect(new TextDecoder().decode(read)).toBe("linked")
}),
),
)
+1 -1
View File
@@ -8481,7 +8481,7 @@
}
}
},
"description": "Serve one file relative to the requested location.",
"description": "Stream one file relative to the requested location with ETag, Last-Modified, and single-range HTTP Range support (206 Partial Content, 416 Range Not Satisfiable, and 304 Not Modified; multi-range and malformed Range headers fall back to 200).",
"summary": "Read file"
}
},
+2 -1
View File
@@ -39,7 +39,8 @@ export const FileSystemGroup = HttpApiGroup.make("server.fs")
OpenApi.annotations({
identifier: "fs.read",
summary: "Read file",
description: "Serve one file relative to the requested location.",
description:
"Stream one file relative to the requested location with ETag, Last-Modified, and single-range HTTP Range support (206 Partial Content, 416 Range Not Satisfiable, and 304 Not Modified; multi-range and malformed Range headers fall back to 200).",
}),
),
)
+202 -7
View File
@@ -2,29 +2,29 @@ import { FileSystem } from "@opencode/core/filesystem"
import { RelativePath } from "@opencode/core/schema"
import { FileNotFoundError } from "@opencode/protocol/errors"
import { Effect } from "effect"
import { HttpServerResponse } from "effect/unstable/http"
import { HttpEffect, HttpPlatform, type HttpServerRequest, HttpServerResponse } from "effect/unstable/http"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Api } from "../api"
import { response } from "../location"
export const FileSystemHandler = HttpApiBuilder.group(Api, "server.fs", (handlers) =>
Effect.gen(function* () {
const platform = yield* HttpPlatform.HttpPlatform
return handlers
.handleRaw("fs.read", (ctx) =>
Effect.gen(function* () {
const path = yield* decodeRequestPath(ctx.request.url)
const fs = yield* FileSystem.Service
const file = yield* fs
.read({
path: RelativePath.make(
decodeURIComponent(new URL(ctx.request.url, "http://localhost").pathname.slice(13)),
),
})
.read({ path })
.pipe(
Effect.mapError(
(error) => new FileNotFoundError({ path: error.path, message: `File not found: ${error.path}` }),
),
)
return HttpServerResponse.uint8Array(file.content, { contentType: file.mime })
return yield* serveFile(ctx.request, path, file).pipe(
Effect.provideService(HttpPlatform.HttpPlatform, platform),
)
}),
)
.handle("fs.list", (ctx) =>
@@ -53,3 +53,198 @@ export const FileSystemHandler = HttpApiBuilder.group(Api, "server.fs", (handler
)
}),
)
function decodeRequestPath(url: string) {
const raw = new URL(url, "http://localhost").pathname.slice(13)
return Effect.try({
try: () => RelativePath.make(decodeURIComponent(raw)),
catch: () => new FileNotFoundError({ path: raw, message: `File not found: ${raw}` }),
})
}
const serveFile = Effect.fnUntraced(function* (
request: HttpServerRequest.HttpServerRequest,
path: RelativePath,
file: FileSystem.File,
) {
yield* HttpEffect.appendPreResponseHandler((_request, res) =>
Effect.succeed(HttpServerResponse.removeHeader(res, "content-encoding")),
)
const fileResponse = (options?: Parameters<typeof HttpServerResponse.file>[1]) =>
HttpServerResponse.file(file.path, {
...options,
headers: {
"content-type": file.mime,
"accept-ranges": "bytes",
"content-encoding": "identity",
...options?.headers,
},
}).pipe(
Effect.catchReason(
"PlatformError",
"NotFound",
() => Effect.fail(new FileNotFoundError({ path, message: `File not found: ${path}` })),
(_, error) => Effect.die(error),
),
)
const rangeHeader = request.method === "GET" ? request.headers["range"] : undefined
const ifRange = request.headers["if-range"]
const shouldEvaluateConditionals =
request.headers["if-none-match"] !== undefined || request.headers["if-modified-since"] !== undefined
const fullResponse =
shouldEvaluateConditionals || (rangeHeader !== undefined && ifRange !== undefined) || rangeHeader === undefined
? yield* fileResponse()
: undefined
if (shouldEvaluateConditionals && fullResponse !== undefined) {
const conditional = evaluateConditionalRequest(request, fullResponse)
if (conditional !== undefined) return conditional
}
if (rangeHeader === undefined) {
return fullResponse ?? (yield* fileResponse())
}
if (ifRange !== undefined && fullResponse !== undefined && !matchesIfRange(ifRange, fullResponse)) {
return fullResponse
}
const parsedRange = parseRange(rangeHeader, file.size)
if (parsedRange === undefined) {
return fullResponse ?? (yield* fileResponse())
}
if (parsedRange === "unsatisfiable") {
return HttpServerResponse.empty({
status: 416,
headers: {
"accept-ranges": "bytes",
"content-range": `bytes */${file.size}`,
},
})
}
return yield* fileResponse({
status: 206,
offset: parsedRange.start,
bytesToRead: parsedRange.end - parsedRange.start + 1,
headers: {
"content-range": `bytes ${parsedRange.start}-${parsedRange.end}/${file.size}`,
},
})
})
function parseInteger(value: string) {
if (!/^\d+$/.test(value)) return undefined
const parsed = Number(value)
return Number.isSafeInteger(parsed) ? parsed : undefined
}
function parseRange(
header: string,
fileSize: number,
): { readonly start: number; readonly end: number } | "unsatisfiable" | undefined {
const value = header.trim()
if (!value.toLowerCase().startsWith("bytes=")) return undefined
const rangeValue = value.slice(6).trim()
if (rangeValue.length === 0 || rangeValue.includes(",")) return undefined
const separatorIndex = rangeValue.indexOf("-")
if (separatorIndex === -1) return undefined
const startPart = rangeValue.slice(0, separatorIndex).trim()
const endPart = rangeValue.slice(separatorIndex + 1).trim()
if (startPart === "" && endPart === "") return undefined
if (startPart === "") {
const suffixLength = parseInteger(endPart)
if (suffixLength === undefined) return undefined
if (suffixLength === 0 || fileSize === 0) return "unsatisfiable"
return {
start: Math.max(fileSize - suffixLength, 0),
end: fileSize - 1,
}
}
const start = parseInteger(startPart)
if (start === undefined) return undefined
if (endPart === "") {
if (start >= fileSize) return "unsatisfiable"
return {
start,
end: fileSize - 1,
}
}
const end = parseInteger(endPart)
if (end === undefined) return undefined
if (start > end || start >= fileSize) return "unsatisfiable"
return {
start,
end: Math.min(end, fileSize - 1),
}
}
function stripWeakEtagPrefix(value: string) {
const trimmed = value.trim()
return /^w\//i.test(trimmed) ? trimmed.slice(2) : trimmed
}
function matchesIfNoneMatch(ifNoneMatch: string, etag: string | undefined) {
const normalizedEtag = etag === undefined ? undefined : stripWeakEtagPrefix(etag)
return ifNoneMatch.split(",").some((candidate) => {
const value = candidate.trim()
if (value === "") return false
if (value === "*") return true
return normalizedEtag !== undefined && stripWeakEtagPrefix(value) === normalizedEtag
})
}
function isNotModifiedSince(ifModifiedSince: string, lastModified: string | undefined) {
if (lastModified === undefined) return false
const ifModifiedSinceMs = Date.parse(ifModifiedSince)
if (Number.isNaN(ifModifiedSinceMs)) return false
const lastModifiedMs = Date.parse(lastModified)
if (Number.isNaN(lastModifiedMs)) return false
return lastModifiedMs <= ifModifiedSinceMs
}
function matchesIfRange(ifRange: string, response: HttpServerResponse.HttpServerResponse) {
const value = ifRange.trim()
if (value === "") return false
if (value.startsWith('"') || /^w\/"/i.test(value)) {
const etag = response.headers["etag"]
if (etag === undefined || !value.endsWith('"')) return false
return stripWeakEtagPrefix(value) === stripWeakEtagPrefix(etag)
}
const lastModified = response.headers["last-modified"]
if (lastModified === undefined) return false
const ifRangeMs = Date.parse(value)
if (Number.isNaN(ifRangeMs)) return false
const lastModifiedMs = Date.parse(lastModified)
if (Number.isNaN(lastModifiedMs)) return false
return lastModifiedMs === ifRangeMs
}
function notModifiedResponse(response: HttpServerResponse.HttpServerResponse) {
return HttpServerResponse.empty({
status: 304,
headers: {
...(response.headers["etag"] !== undefined ? { etag: response.headers["etag"] } : {}),
...(response.headers["cache-control"] !== undefined ? { "cache-control": response.headers["cache-control"] } : {}),
...(response.headers["last-modified"] !== undefined ? { "last-modified": response.headers["last-modified"] } : {}),
},
})
}
function evaluateConditionalRequest(
request: HttpServerRequest.HttpServerRequest,
response: HttpServerResponse.HttpServerResponse,
) {
const ifNoneMatch = request.headers["if-none-match"]
if (ifNoneMatch !== undefined) {
return matchesIfNoneMatch(ifNoneMatch, response.headers["etag"]) ? notModifiedResponse(response) : undefined
}
const ifModifiedSince = request.headers["if-modified-since"]
if (ifModifiedSince !== undefined && isNotModifiedSince(ifModifiedSince, response.headers["last-modified"])) {
return notModifiedResponse(response)
}
return undefined
}
+14 -3
View File
@@ -21,6 +21,7 @@ import { SessionTransfer } from "@opencode/core/session/transfer"
import { ShellSelect } from "@opencode/core/shell/select"
import { Job } from "@opencode/core/job"
import { Mcp } from "@opencode/core/mcp/index"
import { FSUtil } from "@opencode/util/fs-util"
import { Global } from "@opencode/util/global"
import { InstructionDiscovery } from "@opencode/core/instruction-discovery"
import { LocationServiceMap } from "@opencode/core/location-service-map"
@@ -32,9 +33,9 @@ import { SdkPlugins } from "@opencode/core/plugin/sdk"
import { WellKnown } from "@opencode/core/wellknown"
import { Workspace } from "@opencode/core/workspace"
import { Watcher } from "@opencode/core/filesystem/watcher"
import { HttpRouter } from "effect/unstable/http"
import { HttpPlatform, HttpRouter } from "effect/unstable/http"
import { HttpApiBuilder } from "effect/unstable/httpapi"
import { Context, Effect, Layer, Option } from "effect"
import { Context, Effect, FileSystem, Layer, Option } from "effect"
import { Api } from "./api"
import { ServerAuth } from "./auth"
import { CorsConfig } from "./cors"
@@ -51,6 +52,7 @@ import type { ServerOptions } from "./options"
const applicationServiceNodes = [
Global.node,
FSUtil.node,
Database.node,
Bus.node,
EventLogger.node,
@@ -160,6 +162,9 @@ function makeRoutes<AuthError, AuthServices>(
return serviceLayer.pipe(
Layer.flatMap((context) => {
const services = Layer.succeedContext(context)
const platform = Layer.fresh(HttpPlatform.layer).pipe(
Layer.provide(Layer.succeed(FileSystem.FileSystem, Context.get(context, FSUtil.Service))),
)
const requestServices = Layer.merge(
Layer.succeedContext(
Context.pick(
@@ -174,7 +179,13 @@ function makeRoutes<AuthError, AuthServices>(
ServerInfo.layer(serviceURLs, Context.get(context, Global.Service).tmp, options.app),
)
const api = HttpApiBuilder.layer(Api, { openapiPath: "/openapi.json" }).pipe(
Layer.provide(handlers.pipe(Layer.provide(services), Layer.provide(Layer.succeed(CorsConfig, options)))),
Layer.provide(
handlers.pipe(
Layer.provide(platform),
Layer.provide(services),
Layer.provide(Layer.succeed(CorsConfig, options)),
),
),
Layer.provide(formLocationLayer),
Layer.provide(sessionLocationLayer),
Layer.provide(layer),
+211
View File
@@ -77,3 +77,214 @@ it.live(
}),
15_000,
)
it.live(
"streams files with Range, conditional requests, confinement, and multi-megabyte byte integrity",
() =>
Effect.gen(function* () {
const tmp = yield* tmpdirScoped()
const current = path.join(tmp.path, "project")
const outside = path.join(tmp.path, "outside")
const largeBytes = Uint8Array.from(
{ length: 4 * 1024 * 1024 + 321 },
(_, i) => (i * 31 + (i >> 8)) & 0xff,
)
yield* Effect.promise(async () => {
await fs.mkdir(path.join(current, "nested"), { recursive: true })
await fs.mkdir(outside, { recursive: true })
await fs.writeFile(path.join(current, "clip.mp4"), "0123456789")
await fs.writeFile(path.join(current, "empty.bin"), "")
await fs.writeFile(path.join(current, "large.mp4"), largeBytes)
await fs.writeFile(path.join(outside, "secret.txt"), "secret")
await fs.symlink(outside, path.join(current, "escape-link"), "junction")
})
const server = yield* startServer(path.join(tmp.path, "config"))
const readUrl = (relativePath: string) => {
const url = new URL(`/api/fs/read/${relativePath}`, server.base)
url.searchParams.set("location[directory]", current)
return url
}
const request = (relativePath: string, init?: RequestInit) =>
Effect.promise(() =>
fetch(readUrl(relativePath), {
...init,
headers: {
...server.headers,
...init?.headers,
},
}),
)
// 1. Full 200 read headers and body
const full = yield* request("clip.mp4")
expect(full.status).toBe(200)
expect(full.headers.get("accept-ranges")).toBe("bytes")
expect(full.headers.get("content-length")).toBe("10")
expect(full.headers.get("content-type")).toBe("video/mp4")
expect(full.headers.get("content-encoding")).toBeNull()
const etag = full.headers.get("etag")
const lastModified = full.headers.get("last-modified")
expect(etag).toMatch(/^W\/"[0-9a-f]+-[0-9a-f]+"$/)
expect(lastModified).toBeTruthy()
expect(yield* Effect.promise(() => full.text())).toBe("0123456789")
// 2. HEAD probe (and HEAD with Range ignores Range)
const head = yield* request("clip.mp4", { method: "HEAD", headers: { Range: "bytes=0-3" } })
expect(head.status).toBe(200)
expect(head.headers.get("accept-ranges")).toBe("bytes")
expect(head.headers.get("content-length")).toBe("10")
expect(head.headers.get("content-type")).toBe("video/mp4")
expect(head.headers.get("content-range")).toBeNull()
expect(head.headers.get("etag")).toBe(etag)
expect(head.headers.get("last-modified")).toBe(lastModified)
expect(yield* Effect.promise(() => head.text())).toBe("")
// 3. Explicit, open-ended, clamped, and suffix Range forms -> 206 Partial Content
yield* Effect.forEach(
[
["bytes=0-0", "0", "bytes 0-0/10"],
["bytes=2-5", "2345", "bytes 2-5/10"],
["bytes=7-99", "789", "bytes 7-9/10"],
["bytes=6-", "6789", "bytes 6-9/10"],
["bytes=-3", "789", "bytes 7-9/10"],
["bytes=-99", "0123456789", "bytes 0-9/10"],
] as const,
([range, expectedBody, expectedContentRange]) =>
Effect.gen(function* () {
const partial = yield* request("clip.mp4", { headers: { Range: range } })
expect({ range, status: partial.status }).toEqual({ range, status: 206 })
expect(partial.headers.get("accept-ranges")).toBe("bytes")
expect(partial.headers.get("content-range")).toBe(expectedContentRange)
expect(partial.headers.get("content-length")).toBe(String(expectedBody.length))
expect(partial.headers.get("content-type")).toBe("video/mp4")
expect(partial.headers.get("etag")).toBe(etag)
expect(partial.headers.get("last-modified")).toBe(lastModified)
expect(yield* Effect.promise(() => partial.text())).toBe(expectedBody)
}),
)
// 4. Unsatisfiable Range -> 416 with Content-Range: bytes */size
yield* Effect.forEach(
[
["clip.mp4", "bytes=10-12", 10],
["clip.mp4", "bytes=10-", 10],
["clip.mp4", "bytes=5-2", 10],
["clip.mp4", "bytes=-0", 10],
["empty.bin", "bytes=0-0", 0],
] as const,
([file, range, total]) =>
Effect.gen(function* () {
const unsatisfiable = yield* request(file, { headers: { Range: range } })
expect({ file, range, status: unsatisfiable.status }).toEqual({ file, range, status: 416 })
expect(unsatisfiable.headers.get("content-range")).toBe(`bytes */${total}`)
expect(yield* Effect.promise(() => unsatisfiable.text())).toBe("")
}),
)
// 5. Multi-range and malformed Range headers fall back to full 200
yield* Effect.forEach(
["bytes=0-1, 4-5", "items=0-3", "bytes=", "bytes=-", "bytes=abc-def", "bytes=0-1-2"] as const,
(range) =>
Effect.gen(function* () {
const fallback = yield* request("clip.mp4", { headers: { Range: range } })
expect({ range, status: fallback.status }).toEqual({ range, status: 200 })
expect(fallback.headers.get("content-range")).toBeNull()
expect(fallback.headers.get("content-length")).toBe("10")
expect(yield* Effect.promise(() => fallback.text())).toBe("0123456789")
}),
)
// 6. Conditional requests: If-None-Match & If-Modified-Since -> 304 Not Modified
if (!etag || !lastModified) return yield* Effect.die(new Error("Missing ETag or Last-Modified"))
const strongEtag = etag.slice(2)
yield* Effect.forEach(
[
["If-None-Match", etag],
["If-None-Match", strongEtag],
["If-None-Match", `"other", ${etag}`],
["If-None-Match", "*"],
["If-Modified-Since", lastModified],
] as const,
([header, value]) =>
Effect.gen(function* () {
const notModified = yield* request("clip.mp4", { headers: { [header]: value } })
expect(notModified.status).toBe(304)
expect(notModified.headers.get("etag")).toBe(etag)
expect(notModified.headers.get("last-modified")).toBe(lastModified)
expect(yield* Effect.promise(() => notModified.text())).toBe("")
}),
)
const modifiedSincePast = yield* request("clip.mp4", {
headers: { "If-Modified-Since": "Wed, 01 Jan 2020 00:00:00 GMT" },
})
expect(modifiedSincePast.status).toBe(200)
expect(yield* Effect.promise(() => modifiedSincePast.text())).toBe("0123456789")
// Mismatched If-None-Match takes precedence over matching If-Modified-Since
const mismatchIfNoneMatch = yield* request("clip.mp4", {
headers: { "If-None-Match": 'W/"mismatch"', "If-Modified-Since": lastModified },
})
expect(mismatchIfNoneMatch.status).toBe(200)
expect(yield* Effect.promise(() => mismatchIfNoneMatch.text())).toBe("0123456789")
// 7. If-Range matching -> 206; If-Range mismatch -> full 200
yield* Effect.forEach([etag, strongEtag, lastModified], (ifRange) =>
Effect.gen(function* () {
const matchedRange = yield* request("clip.mp4", {
headers: { Range: "bytes=1-4", "If-Range": ifRange },
})
expect(matchedRange.status).toBe(206)
expect(matchedRange.headers.get("content-range")).toBe("bytes 1-4/10")
expect(yield* Effect.promise(() => matchedRange.text())).toBe("1234")
}),
)
yield* Effect.forEach(['W/"stale"', '"stale"', "Wed, 01 Jan 2020 00:00:00 GMT", "not-a-date"], (ifRange) =>
Effect.gen(function* () {
const mismatchedRange = yield* request("clip.mp4", {
headers: { Range: "bytes=1-4", "If-Range": ifRange },
})
expect(mismatchedRange.status).toBe(200)
expect(mismatchedRange.headers.get("content-range")).toBeNull()
expect(mismatchedRange.headers.get("content-length")).toBe("10")
expect(yield* Effect.promise(() => mismatchedRange.text())).toBe("0123456789")
}),
)
// 8. Confinement enforced (404 FileNotFoundError instead of 500)
yield* Effect.forEach(["..%2Foutside%2Fsecret.txt", "escape-link/secret.txt", "nested"], (forbiddenPath) =>
Effect.gen(function* () {
const escaped = yield* request(forbiddenPath)
expect({ forbiddenPath, status: escaped.status }).toEqual({ forbiddenPath, status: 404 })
expect(yield* Effect.promise(() => escaped.json())).toMatchObject({
_tag: "FileNotFoundError",
})
}),
)
// 9. Multi-megabyte file streams byte-for-byte on full read and range slice
const fullLarge = yield* request("large.mp4")
expect(fullLarge.status).toBe(200)
expect(fullLarge.headers.get("content-length")).toBe(String(largeBytes.length))
const fullLargeReceived = new Uint8Array(yield* Effect.promise(() => fullLarge.arrayBuffer()))
expect(fullLargeReceived.length).toBe(largeBytes.length)
expect(Buffer.compare(fullLargeReceived, largeBytes)).toBe(0)
const sliceStart = 1024 * 1024 + 17
const sliceEnd = 2 * 1024 * 1024 + 519
const partialLarge = yield* request("large.mp4", {
headers: { Range: `bytes=${sliceStart}-${sliceEnd}` },
})
expect(partialLarge.status).toBe(206)
expect(partialLarge.headers.get("content-range")).toBe(
`bytes ${sliceStart}-${sliceEnd}/${largeBytes.length}`,
)
expect(partialLarge.headers.get("content-length")).toBe(String(sliceEnd - sliceStart + 1))
const partialLargeReceived = new Uint8Array(yield* Effect.promise(() => partialLarge.arrayBuffer()))
expect(Buffer.compare(partialLargeReceived, largeBytes.subarray(sliceStart, sliceEnd + 1))).toBe(0)
}),
15_000,
)
+1 -1
View File
@@ -8481,7 +8481,7 @@
}
}
},
"description": "Serve one file relative to the requested location.",
"description": "Stream one file relative to the requested location with ETag, Last-Modified, and single-range HTTP Range support (206 Partial Content, 416 Range Not Satisfiable, and 304 Not Modified; multi-range and malformed Range headers fall back to 200).",
"summary": "Read file"
}
},
+1 -1
View File
@@ -8481,7 +8481,7 @@
}
}
},
"description": "Serve one file relative to the requested location.",
"description": "Stream one file relative to the requested location with ETag, Last-Modified, and single-range HTTP Range support (206 Partial Content, 416 Range Not Satisfiable, and 304 Not Modified; multi-range and malformed Range headers fall back to 200).",
"summary": "Read file"
}
},