Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
114 changes: 114 additions & 0 deletions apps/content/docs/plugins/static-file.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
---
title: "Static File Plugin"
description: "Use StaticFileHandlerPlugin to serve static files alongside your procedures with standard HTTP semantics: ETag and Last-Modified conditional requests, range requests, index files, and directory traversal protection."
sidebar:
label: "Static File"
---

## Installation

```package-install
npm install @orpc/node@beta
```

## How It Works

After routing, when no procedure matches a GET or HEAD request, the plugin maps the request path to a file inside `rootDir` and serves it. Matched procedures always take precedence. Requests that resolve to a directory are redirected to their trailing slash form and answered with the directory's `index.html`.

Every file response carries an [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/ETag) and [Last-Modified](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Last-Modified) header, so clients sending `If-None-Match` or `If-Modified-Since` receive `304 Not Modified` when the file is unchanged, and `If-Match` or `If-Unmodified-Since` receive `412 Precondition Failed`. Single [range requests](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Range_requests) are answered with `206 Partial Content`, which enables media seeking and resumable downloads.

Dot segments like `..` are resolved in URL space, following the [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-5.2.4) normalization browsers and proxies apply. One that would climb above the served path is refused rather than clamped, so a proxy in front of the handler can never disagree with it about which path was requested, and the resolved path is checked against `rootDir` again before any file is opened. Symbolic links whose target leaves `rootDir` are refused unless `allowSymlinks` is set. Dotfiles are treated as not found unless explicitly enabled, which is a request-path policy, so it does not apply to a configured `indexFile` or `fallbackFile`.

:::warning
`rootDir` should contain only files you intend to make public. Anything reachable inside it is served, including a file a request happens to name.
:::

## Setup

The plugin reads files through the Node.js filesystem API but only interacts with the handler through standard oRPC interfaces, so it works with any handler on a Node.js compatible runtime, whether it uses the [Node HTTP Adapter](/docs/adapters/node-http) or the [Fetch API Adapter](/docs/adapters/fetch-api).

```ts
import { StaticFileHandlerPlugin } from '@orpc/node'
import { RPCHandler } from '@orpc/server/node'

const handler = new RPCHandler(router, {
plugins: [
new StaticFileHandlerPlugin({
/**
* The directory files are served from. Resolved against the working
* directory when relative.
*/
rootDir: './public',

/**
* The URL path files are served under, appended to the handler prefix
* when one is set.
*
* @default '/'
*/
path: '/',

/**
* The file served when the request path resolves to a directory.
* Set to `false` to disable directory index files.
*
* @default 'index.html'
*/
indexFile: 'index.html',

/**
* A file served with status 200 when no file matches the request path,
* relative to `rootDir`. Useful for single-page application routing.
*
* @default undefined
*/
fallbackFile: 'index.html',

/**
* The `Cache-Control` response header value. Set to `false` to omit the header.
*
* @default 'public, max-age=0'
*/
cacheControl: 'public, max-age=0',

/**
* Whether files and directories whose name starts with a dot can be served.
*
* @default false
*/
dotfiles: false,

/**
* Whether precompressed sidecar files (`.br`, `.zst`, `.gz`) can be served
* when the client accepts their encoding and the content type is compressible.
*
* @default false
*/
precompressed: false,

/**
* Whether symbolic links whose target lies outside `rootDir` can be served.
* Enabling this makes every file the links reach publicly readable.
*
* @default false
*/
allowSymlinks: false,

/**
* Extra content types keyed by lowercase file extension without the dot,
* merged over the built-in mapping. Unknown extensions are served as
* `application/octet-stream`.
*/
mimeTypes: {},
}),
],
})
```

:::info
When the handler is served under a [prefix](/docs/rpc/handler), files are only reachable inside that prefix, because a handler never intercepts requests outside its prefix.
:::

## Learn More

For implementation details, see the [source code](https://github.com/middleapi/orpc/blob/main/packages/node/src/static-file-handler-plugin.ts).
67 changes: 67 additions & 0 deletions benches/static-file-handler.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import type { StandardLazyRequest } from '@standardserver/core'
import { Buffer } from 'node:buffer'
import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'
import { StaticFileHandlerPlugin } from '@orpc/node'
import { RPCHandlerCodec, StandardHandler } from '@orpc/server/standard'
import { bench } from 'vitest'
import { drainBody } from './__shared__/payloads'

const rootDir = mkdtempSync(path.join(tmpdir(), 'orpc-static-file-bench-'))

writeFileSync(path.join(rootDir, 'file.txt'), Buffer.alloc(10 * 1024, 'a'))
mkdirSync(path.join(rootDir, 'deeply', 'nested', 'dir'), { recursive: true })
writeFileSync(path.join(rootDir, 'deeply', 'nested', 'dir', 'file.txt'), Buffer.alloc(10 * 1024, 'a'))

const handler = new StandardHandler(new RPCHandlerCodec({}, {}), {
plugins: [new StaticFileHandlerPlugin({ rootDir })],
})

/** Skips the symlink containment check, which costs one `realpath` per lookup. */
const trustedHandler = new StandardHandler(new RPCHandlerCodec({}, {}), {
plugins: [new StaticFileHandlerPlugin({ rootDir, allowSymlinks: true })],
})

function createRequest(url: `/${string}`, headers: Record<string, string> = {}): StandardLazyRequest {
return {
url,
method: 'GET',
headers,
resolveBody: () => Promise.resolve(undefined),
}
}

const { response } = await handler.handle(createRequest('/file.txt'), { context: {} })
await drainBody(response!.body)
const etag = response!.headers.etag as string

describe('static file handler plugin', () => {
bench('serve 10kb file', async () => {
const { response } = await handler.handle(createRequest('/file.txt'), { context: {} })
await drainBody(response!.body)
})

bench('serve deeply nested encoded path', async () => {
const { response } = await handler.handle(createRequest('/deeply/nested/dir/file%2etxt'), { context: {} })
await drainBody(response!.body)
})

bench('range request', async () => {
const { response } = await handler.handle(createRequest('/file.txt', { range: 'bytes=0-1023' }), { context: {} })
await drainBody(response!.body)
})

bench('not modified (304)', async () => {
await handler.handle(createRequest('/file.txt', { 'if-none-match': etag }), { context: {} })
})

bench('not found fall through', async () => {
await handler.handle(createRequest('/missing/file.txt'), { context: {} })
})

bench('serve 10kb file (allowSymlinks)', async () => {
const { response } = await trustedHandler.handle(createRequest('/file.txt'), { context: {} })
await drainBody(response!.body)
})
})
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
"@orpc/experimental-effect": "workspace:*",
"@orpc/json-schema": "workspace:*",
"@orpc/next": "workspace:*",
"@orpc/node": "workspace:*",
"@orpc/openapi": "workspace:*",
"@orpc/opentelemetry": "workspace:*",
"@orpc/pino": "workspace:*",
Expand Down
1 change: 1 addition & 0 deletions packages/ai-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
1 change: 1 addition & 0 deletions packages/arktype/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
1 change: 1 addition & 0 deletions packages/bun/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
1 change: 1 addition & 0 deletions packages/client/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
1 change: 1 addition & 0 deletions packages/cloudflare/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
1 change: 1 addition & 0 deletions packages/contract/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
1 change: 1 addition & 0 deletions packages/effect/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
1 change: 1 addition & 0 deletions packages/evlog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
1 change: 1 addition & 0 deletions packages/hibernation/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
1 change: 1 addition & 0 deletions packages/json-schema/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
1 change: 1 addition & 0 deletions packages/nest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ You can read the documentation [here](https://orpc.dev).
- [@orpc/swr](https://www.npmjs.com/package/@orpc/swr): Integrate with [SWR](https://swr.vercel.app/).
- [@orpc/experimental-effect](https://www.npmjs.com/package/@orpc/experimental-effect): Integrate with [Effect](https://effect.website/).
- [@orpc/nest](https://www.npmjs.com/package/@orpc/nest): Implement your contract with [NestJS](https://nestjs.com/).
- [@orpc/node](https://www.npmjs.com/package/@orpc/node): [Node.js](https://nodejs.org/) plugins, helpers, like serving static files.
- [@orpc/bun](https://www.npmjs.com/package/@orpc/bun): Adapters for [Bun's Redis](https://bun.sh/).
- [@orpc/cloudflare](https://www.npmjs.com/package/@orpc/cloudflare): Adapters for [Cloudflare's RateLimit and Durable Objects](https://developers.cloudflare.com/workers/).
- [@orpc/trpc](https://www.npmjs.com/package/@orpc/trpc): Reuse existing [tRPC](https://trpc.io/) routers within oRPC.
Expand Down
Loading
Loading