Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
102 changes: 102 additions & 0 deletions apps/content/docs/plugins/static-file.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
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 a weak [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. 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 and clamped at the served directory, following the [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986#section-5.2.4) normalization browsers and proxies apply, so a request can never read outside `rootDir`. Dotfiles are treated as not found unless explicitly enabled.

## 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,

/**
* 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).
57 changes: 57 additions & 0 deletions benches/static-file-handler.bench.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
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 })],
})

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: {} })
})
})
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
1 change: 1 addition & 0 deletions packages/next/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