Skip to content
Draft
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
39 changes: 35 additions & 4 deletions apps/ottabase-template-app-tanstack/cloudflare-worker.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { RealtimeActor } from '@ottabase/cf-realtime/server';
import { errorResponse, ServiceError } from '@ottabase/utils/http-errors';
import { ErrorPage } from '@ottabase/error-page';
import type { CloudflareEnv } from './cloudflare-env';
import { queueHandler } from './ottabase/queue';
import { handleBootstrapRoute, interceptIfNotReady, resolvePlatformState } from './worker/bootstrap';
Expand Down Expand Up @@ -140,12 +141,42 @@ export default {
} catch (err) {
console.error('Worker unhandled error:', err);

if (err instanceof ServiceError) {
return errorResponse(err.message, err.status, err.toApiResponse());
const status = err instanceof ServiceError ? err.status : 500;
const isDev =
!(env as Record<string, unknown>).ENVIRONMENT ||
(env as Record<string, unknown>).ENVIRONMENT === 'development' ||
(env as Record<string, unknown>).ENVIRONMENT === 'dev';

// For non-HTML requests (API calls), always return JSON
if (!isHtmlRequest(request)) {
if (err instanceof ServiceError) {
return errorResponse(err.message, err.status, err.toApiResponse());
}
return errorResponse(
isDev && err instanceof Error ? err.message : 'An unexpected error occurred',
500,
{ code: 'INTERNAL_SERVER_ERROR' },
);
}

// For HTML requests: show detailed dev error page or clean production page
const errorPage = new ErrorPage();
if (isDev) {
errorPage.addRequestMetadata(request);
const html = errorPage.toHTML(err, { title: 'Worker Error' });
return new Response(html, {
status,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}

return errorResponse(err instanceof Error ? err.message : 'An unexpected error occurred', 500, {
code: 'INTERNAL_SERVER_ERROR',
// Production: minimal error page without internal details
const html = errorPage.toProductionHTML(status, {
title: err instanceof ServiceError ? err.message : undefined,
});
return new Response(html, {
status,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
},
Expand Down
1 change: 1 addition & 0 deletions apps/ottabase-template-app-tanstack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"@ottabase/ui-shadcn": "workspace:*",
"@ottabase/ui-split-pane": "workspace:*",
"@ottabase/utils": "workspace:*",
"@ottabase/error-page": "workspace:*",
"@tabler/icons-react": "catalog:",
"@tanstack/react-query": "catalog:",
"@tanstack/react-router": "catalog:",
Expand Down
2 changes: 2 additions & 0 deletions packages/error-page/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
dist/
.turbo/
149 changes: 149 additions & 0 deletions packages/error-page/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
# @ottabase/error-page

Pretty print JavaScript errors as self-contained HTML pages — edge-runtime compatible.

Built for Cloudflare Workers and other edge runtimes where Node.js `fs` is unavailable.

## Features

- 🔥 Beautiful error pages with error type badges, expandable stack frames, and "Open in editor" links
- 🌗 Dark/light theme toggle (respects `prefers-color-scheme`)
- 📋 Request metadata display (method, URL, headers with sensitive masking)
- ⛓️ Error cause chain rendering (recursive)
- 🏷️ Extra error properties shown as inline badges
- 📄 Stack Trace / Raw JSON tab switching with copy-to-clipboard
- 🕒 Timestamp display for when the error occurred
- 🛡️ XSS-safe HTML escaping, circular reference handling
- ☁️ Zero Node.js dependencies — works in Cloudflare Workers and other edge runtimes

## Usage

```ts
import { ErrorPage } from '@ottabase/error-page';

try {
await handleRequest();
} catch (error) {
const errorPage = new ErrorPage();
errorPage.addRequestMetadata(request);

const html = errorPage.toHTML(error, { title: 'Worker Error' });
return new Response(html, {
status: 500,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
```

## API

### `new ErrorPage()`

Create a new ErrorPage instance.

### `errorPage.toHTML(error, options?)`

Render an error to a self-contained HTML page with:

- Error type badge and message
- Expandable stack trace frames (app frames highlighted)
- Raw JSON error view with copy-to-clipboard
- Error cause chain
- Metadata sections
- Dark/light theme toggle
- "Open in editor" links
- Timestamp

**Options:**

| Option | Type | Default | Description |
| ---------- | -------- | ------------------------- | -------------------------------------- |
| `title` | `string` | `"An error has occurred"` | Page title / subtitle |
| `ide` | `string` | `"vscode"` | Code editor for file links |
| `offset` | `number` | `0` | Stack frames to skip |
| `cspNonce` | `string` | — | CSP nonce for inline style/script tags |

Supported editors: `vscode`, `sublime`, `atom`, `phpstorm`, `textmate`, `emacs`, `macvim`, or a custom URL template with
`%f`, `%l`, `%c` placeholders.

### `errorPage.group(name, sections)`

Add metadata sections (e.g., request info, environment).

```ts
errorPage.group('Request', {
info: [
{ key: 'Method', value: 'POST' },
{ key: 'URL', value: '/api/users' },
],
headers: [
{ key: 'content-type', value: 'application/json' },
{ key: 'user-agent', value: request.headers.get('user-agent') },
],
});
```

### `errorPage.addRequestMetadata(request)`

Automatically extract method, URL, and common headers from a `Request` object. Sensitive headers (`authorization`,
`cookie`) are automatically masked.

### `errorPage.toProductionHTML(status, options?)`

Render a minimal, production-safe error page. Does not expose stack traces, internal paths, or error details.

```ts
const html = errorPage.toProductionHTML(500, {
title: 'Service Unavailable',
message: 'We are currently undergoing maintenance.',
});
```

### `errorPage.parse(error, offset?)`

Parse an error into a structured `ParsedError` object without rendering HTML.

### `parseError(error, offset?)`

Standalone function to parse errors into structured objects.

### `renderHTML(parsedError, metadata?, options?)`

Standalone function to render a `ParsedError` to HTML.

### `renderProductionHTML(status, options?)`

Standalone function to render a minimal production error page.

## Integration with Cloudflare Worker

```ts
import { ErrorPage } from '@ottabase/error-page';

export default {
async fetch(request: Request, env: Env): Promise<Response> {
try {
return await handleRequest(request, env);
} catch (err) {
const isDev = !env.ENVIRONMENT || env.ENVIRONMENT === 'development';
const errorPage = new ErrorPage();

if (isDev) {
errorPage.addRequestMetadata(request);
const html = errorPage.toHTML(err, { title: 'Worker Error' });
return new Response(html, {
status: 500,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}

// Production: clean error page without internals
const html = errorPage.toProductionHTML(500);
return new Response(html, {
status: 500,
headers: { 'Content-Type': 'text/html; charset=utf-8' },
});
}
},
};
```
51 changes: 51 additions & 0 deletions packages/error-page/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
{
"name": "@ottabase/error-page",
"version": "0.0.1",
"description": "Pretty print JavaScript errors as self-contained HTML pages — edge-runtime compatible",
"author": "Ottabase",
"license": "MIT",
"type": "module",
"sideEffects": false,
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
},
"files": [
"dist",
"README.md"
],
"keywords": [
"error",
"pretty-error",
"error-page",
"edge-runtime",
"cloudflare-workers",
"ottabase"
],
"scripts": {
"build": "tsup",
"dev": "tsup --watch",
"lint": "eslint src --ext .ts",
"type-check": "tsc --noEmit",
"test": "vitest",
"test:coverage": "vitest --coverage",
"clean": "rimraf dist"
},
"devDependencies": {
"@types/node": "catalog:",
"eslint": "catalog:",
"rimraf": "catalog:",
"tsup": "catalog:",
"typescript": "catalog:",
"vitest": "catalog:"
},
"publishConfig": {
"access": "public"
}
}
Loading