├── .env.example
├── .eslintrc
├── .gitignore
├── .prettierrc
├── LICENSE.md
├── README.md
├── app
├── entry.client.tsx
├── entry.server.tsx
├── root.tsx
└── routes
│ ├── _index.tsx
│ └── bug.tsx
├── package-lock.json
├── package.json
├── patches
└── @remix-run+server-runtime+1.16.1.patch
├── public
└── favicon.ico
├── remix.config.js
├── remix.env.d.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | BUGSNAG_API_KEY=your_api_key
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": ["@remix-run/eslint-config", "@remix-run/eslint-config/node"]
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | /.cache
4 | /build
5 | /public/build
6 | .env
7 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "semi": false,
3 | "singleQuote": true,
4 | "trailingComma": "all",
5 | "arrowParens": "avoid"
6 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 |
2 | The MIT License (MIT)
3 |
4 | Copyright (c) 2022 kiliman
5 |
6 | Permission is hereby granted, free of charge, to any person obtaining a copy
7 | of this software and associated documentation files (the "Software"), to deal
8 | in the Software without restriction, including without limitation the rights
9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | copies of the Software, and to permit persons to whom the Software is
11 | furnished to do so, subject to the following conditions:
12 |
13 | The above copyright notice and this permission notice shall be included in all
14 | copies or substantial portions of the Software.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | SOFTWARE.
23 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Remix Error Logging
2 |
3 | > Remix v1.17.0 has introduced the new `handleError()` export from _entry.server.tsx_. You no longer need this patch.
4 |
5 | https://github.com/remix-run/remix/releases/tag/remix%401.17.0
6 |
7 | This example adds a patch to enable server side logging to your
8 | favorite logging service. This one uses [Bugsnag](https://www.bugsnag.com/), but any of them
9 | should work.
10 |
11 | The patch adds a `handleError` export in _entry.server_. This function
12 | will be called with the current request and error object.
13 |
14 | ```ts
15 | // entry.server.tsx
16 |
17 | // initialize Bugsnag with your API key
18 | Bugsnag.start({
19 | apiKey: process.env.BUGSNAG_API_KEY!,
20 | })
21 |
22 | // add handleError export to notify Bugsnag
23 | // args includes the same object as loader and actions
24 | // { request, params, context }
25 | export function handleError(error: unknown, { request, context }: DataFunctionArgs) {
26 | console.log('notify bugsnag')
27 | Bugsnag.setUser(context.getRequestContext().user)
28 | Bugsnag.notify(error, event => {
29 | event.addMetadata('request', {
30 | url: request.url,
31 | method: request.method,
32 | headers: Object.fromEntries(request.headers.entries()),
33 | })
34 | })
35 | }
36 |
37 | // routes/bug.tsx
38 | export async function loader async ({ request, context }: LoaderArgs) => {
39 | const user = await getUser(request)
40 | // set the context used by bugsnag for this request
41 | context.setRequestContext({ user })
42 |
43 | throw new Error('Oops')
44 | }
45 |
46 | // server.js
47 | const getLoadContext = () => ({
48 | _requestContext: {},
49 | getRequestContext: () => this._requestContext,
50 | setRequestContext: ({ user, context, metaData }) => {
51 | this._requestContext = { user, context, metaData }
52 | },
53 | })
54 | ```
55 |
56 | ## 🛠 Installation
57 |
58 | ```bash
59 | npm install -D patch-package
60 | ```
61 |
62 | Copy the patch file from the `patches` folder to your project `patches` folder.
63 | Apply the patch
64 |
65 | ```bash
66 | npx patch-package
67 | ```
68 |
69 | Update your _package.json_ file to include the following postinstall script. This will ensure your patch is automatically applied when you checkout you project.
70 |
71 | ```json
72 | "scripts": {
73 | "postinstall": "patch-package"
74 | }
75 | ```
76 |
--------------------------------------------------------------------------------
/app/entry.client.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * By default, Remix will handle hydrating your app on the client for you.
3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4 | * For more information, see https://remix.run/file-conventions/entry.client
5 | */
6 |
7 | import { RemixBrowser } from '@remix-run/react'
8 | import { startTransition, StrictMode } from 'react'
9 | import { hydrateRoot } from 'react-dom/client'
10 |
11 | startTransition(() => {
12 | hydrateRoot(
13 | document,
14 |
15 |
16 | ,
17 | )
18 | })
19 |
--------------------------------------------------------------------------------
/app/entry.server.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * By default, Remix will handle generating the HTTP Response for you.
3 | * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4 | * For more information, see https://remix.run/file-conventions/entry.server
5 | */
6 |
7 | import { PassThrough } from 'node:stream'
8 |
9 | import type {
10 | AppLoadContext,
11 | DataFunctionArgs,
12 | EntryContext,
13 | } from '@remix-run/node'
14 | import { Response } from '@remix-run/node'
15 | import { RemixServer } from '@remix-run/react'
16 | import isbot from 'isbot'
17 | import { renderToPipeableStream } from 'react-dom/server'
18 |
19 | const ABORT_DELAY = 5_000
20 |
21 | export default function handleRequest(
22 | request: Request,
23 | responseStatusCode: number,
24 | responseHeaders: Headers,
25 | remixContext: EntryContext,
26 | loadContext: AppLoadContext,
27 | ) {
28 | return isbot(request.headers.get('user-agent'))
29 | ? handleBotRequest(
30 | request,
31 | responseStatusCode,
32 | responseHeaders,
33 | remixContext,
34 | )
35 | : handleBrowserRequest(
36 | request,
37 | responseStatusCode,
38 | responseHeaders,
39 | remixContext,
40 | )
41 | }
42 |
43 | function handleBotRequest(
44 | request: Request,
45 | responseStatusCode: number,
46 | responseHeaders: Headers,
47 | remixContext: EntryContext,
48 | ) {
49 | return new Promise((resolve, reject) => {
50 | const { pipe, abort } = renderToPipeableStream(
51 | ,
56 | {
57 | onAllReady() {
58 | const body = new PassThrough()
59 |
60 | responseHeaders.set('Content-Type', 'text/html')
61 |
62 | resolve(
63 | new Response(body, {
64 | headers: responseHeaders,
65 | status: responseStatusCode,
66 | }),
67 | )
68 |
69 | pipe(body)
70 | },
71 | onShellError(error: unknown) {
72 | reject(error)
73 | },
74 | onError(error: unknown) {
75 | responseStatusCode = 500
76 | console.error(error)
77 | },
78 | },
79 | )
80 |
81 | setTimeout(abort, ABORT_DELAY)
82 | })
83 | }
84 |
85 | function handleBrowserRequest(
86 | request: Request,
87 | responseStatusCode: number,
88 | responseHeaders: Headers,
89 | remixContext: EntryContext,
90 | ) {
91 | return new Promise((resolve, reject) => {
92 | const { pipe, abort } = renderToPipeableStream(
93 | ,
98 | {
99 | onShellReady() {
100 | const body = new PassThrough()
101 |
102 | responseHeaders.set('Content-Type', 'text/html')
103 |
104 | resolve(
105 | new Response(body, {
106 | headers: responseHeaders,
107 | status: responseStatusCode,
108 | }),
109 | )
110 |
111 | pipe(body)
112 | },
113 | onShellError(error: unknown) {
114 | reject(error)
115 | },
116 | onError(error: unknown) {
117 | console.error(error)
118 | responseStatusCode = 500
119 | },
120 | },
121 | )
122 |
123 | setTimeout(abort, ABORT_DELAY)
124 | })
125 | }
126 |
127 | export function handleError(error: Error, args: DataFunctionArgs) {
128 | console.error('💣 ----------')
129 | console.error('ERROR', error)
130 | console.error('💣 ----------')
131 | }
132 |
--------------------------------------------------------------------------------
/app/root.tsx:
--------------------------------------------------------------------------------
1 | import { cssBundleHref } from '@remix-run/css-bundle'
2 | import type { LinksFunction } from '@remix-run/node'
3 | import {
4 | Links,
5 | LiveReload,
6 | Meta,
7 | Outlet,
8 | Scripts,
9 | ScrollRestoration,
10 | } from '@remix-run/react'
11 |
12 | export const links: LinksFunction = () => [
13 | ...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : []),
14 | ]
15 |
16 | export default function App() {
17 | return (
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/app/routes/_index.tsx:
--------------------------------------------------------------------------------
1 | import type { V2_MetaFunction } from '@remix-run/node'
2 | import { Link } from '@remix-run/react'
3 |
4 | export const meta: V2_MetaFunction = () => {
5 | return [
6 | { title: 'New Remix App' },
7 | { name: 'description', content: 'Welcome to Remix!' },
8 | ]
9 | }
10 |
11 | export default function Index() {
12 | return (
13 |
14 |
Welcome to Remix Error Handling
15 |
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/app/routes/bug.tsx:
--------------------------------------------------------------------------------
1 | import type { LoaderArgs } from '@remix-run/node'
2 |
3 | export async function loader({ request, context }: LoaderArgs) {
4 | throw new Error('Oops')
5 | }
6 |
7 | export default function () {
8 | return Bug
9 | }
10 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "sideEffects": false,
4 | "scripts": {
5 | "postinstall": "patch-package",
6 | "build": "remix build",
7 | "dev": "remix dev",
8 | "start": "remix-serve build",
9 | "typecheck": "tsc"
10 | },
11 | "dependencies": {
12 | "@remix-run/css-bundle": "^1.16.1",
13 | "@remix-run/node": "^1.16.1",
14 | "@remix-run/react": "^1.16.1",
15 | "@remix-run/serve": "^1.16.1",
16 | "isbot": "^3.6.8",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0"
19 | },
20 | "devDependencies": {
21 | "@remix-run/dev": "^1.16.1",
22 | "@remix-run/eslint-config": "^1.16.1",
23 | "@types/react": "^18.0.35",
24 | "@types/react-dom": "^18.0.11",
25 | "eslint": "^8.38.0",
26 | "patch-package": "^7.0.0",
27 | "typescript": "^5.0.4"
28 | },
29 | "engines": {
30 | "node": ">=14"
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/patches/@remix-run+server-runtime+1.16.1.patch:
--------------------------------------------------------------------------------
1 | diff --git a/node_modules/@remix-run/server-runtime/.DS_Store b/node_modules/@remix-run/server-runtime/.DS_Store
2 | new file mode 100644
3 | index 0000000..4e3dc72
4 | Binary files /dev/null and b/node_modules/@remix-run/server-runtime/.DS_Store differ
5 | diff --git a/node_modules/@remix-run/server-runtime/dist/cookies.js b/node_modules/@remix-run/server-runtime/dist/cookies.js
6 | index 5d3bacb..563676a 100644
7 | --- a/node_modules/@remix-run/server-runtime/dist/cookies.js
8 | +++ b/node_modules/@remix-run/server-runtime/dist/cookies.js
9 | @@ -15,17 +15,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
10 | var cookie = require('cookie');
11 | var warnings = require('./warnings.js');
12 |
13 | -/**
14 | - * A HTTP cookie.
15 | - *
16 | - * A Cookie is a logical container for metadata about a HTTP cookie; its name
17 | - * and options. But it doesn't contain a value. Instead, it has `parse()` and
18 | - * `serialize()` methods that allow a single instance to be reused for
19 | - * parsing/encoding multiple different values.
20 | - *
21 | - * @see https://remix.run/utils/cookies#cookie-api
22 | - */
23 | -
24 | /**
25 | * Creates a logical container for managing a browser cookie from the server.
26 | *
27 | diff --git a/node_modules/@remix-run/server-runtime/dist/data.js b/node_modules/@remix-run/server-runtime/dist/data.js
28 | index a4446e6..90fa7a5 100644
29 | --- a/node_modules/@remix-run/server-runtime/dist/data.js
30 | +++ b/node_modules/@remix-run/server-runtime/dist/data.js
31 | @@ -14,17 +14,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
32 |
33 | var responses = require('./responses.js');
34 |
35 | -/**
36 | - * An object of unknown type for route loaders and actions provided by the
37 | - * server's `getLoadContext()` function.
38 | - */
39 | -
40 | -/**
41 | - * Data for a route that was returned from a `loader()`.
42 | - *
43 | - * Note: This moves to unknown in ReactRouter and eventually likely in Remix
44 | - */
45 | -
46 | async function callRouteActionRR({
47 | loadContext,
48 | action,
49 | diff --git a/node_modules/@remix-run/server-runtime/dist/errors.js b/node_modules/@remix-run/server-runtime/dist/errors.js
50 | index 1059714..8015172 100644
51 | --- a/node_modules/@remix-run/server-runtime/dist/errors.js
52 | +++ b/node_modules/@remix-run/server-runtime/dist/errors.js
53 | @@ -61,6 +61,7 @@ var mode = require('./mode.js');
54 | * @deprecated in favor of the `ErrorResponse` class in React Router. Please
55 | * enable the `future.v2_errorBoundary` flag to ease your migration to Remix v2.
56 | */
57 | +
58 | function sanitizeError(error, serverMode) {
59 | if (error instanceof Error && serverMode !== mode.ServerMode.Development) {
60 | let sanitized = new Error("Unexpected Server Error");
61 | @@ -79,6 +80,7 @@ function sanitizeErrors(errors, serverMode) {
62 |
63 | // must be type alias due to inference issues on interfaces
64 | // https://github.com/microsoft/TypeScript/issues/15300
65 | +
66 | function serializeError(error, serverMode) {
67 | let sanitized = sanitizeError(error, serverMode);
68 | return {
69 | diff --git a/node_modules/@remix-run/server-runtime/dist/esm/cookies.js b/node_modules/@remix-run/server-runtime/dist/esm/cookies.js
70 | index 93e50c7..ae14b9b 100644
71 | --- a/node_modules/@remix-run/server-runtime/dist/esm/cookies.js
72 | +++ b/node_modules/@remix-run/server-runtime/dist/esm/cookies.js
73 | @@ -11,17 +11,6 @@
74 | import { parse, serialize } from 'cookie';
75 | import { warnOnce } from './warnings.js';
76 |
77 | -/**
78 | - * A HTTP cookie.
79 | - *
80 | - * A Cookie is a logical container for metadata about a HTTP cookie; its name
81 | - * and options. But it doesn't contain a value. Instead, it has `parse()` and
82 | - * `serialize()` methods that allow a single instance to be reused for
83 | - * parsing/encoding multiple different values.
84 | - *
85 | - * @see https://remix.run/utils/cookies#cookie-api
86 | - */
87 | -
88 | /**
89 | * Creates a logical container for managing a browser cookie from the server.
90 | *
91 | diff --git a/node_modules/@remix-run/server-runtime/dist/esm/data.js b/node_modules/@remix-run/server-runtime/dist/esm/data.js
92 | index cd1c615..48b98ab 100644
93 | --- a/node_modules/@remix-run/server-runtime/dist/esm/data.js
94 | +++ b/node_modules/@remix-run/server-runtime/dist/esm/data.js
95 | @@ -10,17 +10,6 @@
96 | */
97 | import { isResponse, json, isDeferredData, isRedirectStatusCode, redirect } from './responses.js';
98 |
99 | -/**
100 | - * An object of unknown type for route loaders and actions provided by the
101 | - * server's `getLoadContext()` function.
102 | - */
103 | -
104 | -/**
105 | - * Data for a route that was returned from a `loader()`.
106 | - *
107 | - * Note: This moves to unknown in ReactRouter and eventually likely in Remix
108 | - */
109 | -
110 | async function callRouteActionRR({
111 | loadContext,
112 | action,
113 | diff --git a/node_modules/@remix-run/server-runtime/dist/esm/errors.js b/node_modules/@remix-run/server-runtime/dist/esm/errors.js
114 | index 7d80756..a8abc21 100644
115 | --- a/node_modules/@remix-run/server-runtime/dist/esm/errors.js
116 | +++ b/node_modules/@remix-run/server-runtime/dist/esm/errors.js
117 | @@ -57,6 +57,7 @@ import { ServerMode } from './mode.js';
118 | * @deprecated in favor of the `ErrorResponse` class in React Router. Please
119 | * enable the `future.v2_errorBoundary` flag to ease your migration to Remix v2.
120 | */
121 | +
122 | function sanitizeError(error, serverMode) {
123 | if (error instanceof Error && serverMode !== ServerMode.Development) {
124 | let sanitized = new Error("Unexpected Server Error");
125 | @@ -75,6 +76,7 @@ function sanitizeErrors(errors, serverMode) {
126 |
127 | // must be type alias due to inference issues on interfaces
128 | // https://github.com/microsoft/TypeScript/issues/15300
129 | +
130 | function serializeError(error, serverMode) {
131 | let sanitized = sanitizeError(error, serverMode);
132 | return {
133 | diff --git a/node_modules/@remix-run/server-runtime/dist/esm/mode.js b/node_modules/@remix-run/server-runtime/dist/esm/mode.js
134 | index 2bea1e0..d82a6c4 100644
135 | --- a/node_modules/@remix-run/server-runtime/dist/esm/mode.js
136 | +++ b/node_modules/@remix-run/server-runtime/dist/esm/mode.js
137 | @@ -11,12 +11,12 @@
138 | /**
139 | * The mode to use when running the server.
140 | */
141 | -let ServerMode = /*#__PURE__*/function (ServerMode) {
142 | +let ServerMode;
143 | +(function (ServerMode) {
144 | ServerMode["Development"] = "development";
145 | ServerMode["Production"] = "production";
146 | ServerMode["Test"] = "test";
147 | - return ServerMode;
148 | -}({});
149 | +})(ServerMode || (ServerMode = {}));
150 | function isServerMode(value) {
151 | return value === ServerMode.Development || value === ServerMode.Production || value === ServerMode.Test;
152 | }
153 | diff --git a/node_modules/@remix-run/server-runtime/dist/esm/responses.js b/node_modules/@remix-run/server-runtime/dist/esm/responses.js
154 | index 661733d..ae9a20e 100644
155 | --- a/node_modules/@remix-run/server-runtime/dist/esm/responses.js
156 | +++ b/node_modules/@remix-run/server-runtime/dist/esm/responses.js
157 | @@ -11,8 +11,6 @@
158 | import { json as json$1, defer as defer$1, redirect as redirect$1 } from '@remix-run/router';
159 | import { serializeError } from './errors.js';
160 |
161 | -// must be a type since this is a subtype of response
162 | -// interfaces must conform to the types they extend
163 | /**
164 | * This is a shortcut for creating `application/json` responses. Converts `data`
165 | * to JSON and sets the `Content-Type` header.
166 | diff --git a/node_modules/@remix-run/server-runtime/dist/esm/routes.js b/node_modules/@remix-run/server-runtime/dist/esm/routes.js
167 | index e32ef4e..c902acb 100644
168 | --- a/node_modules/@remix-run/server-runtime/dist/esm/routes.js
169 | +++ b/node_modules/@remix-run/server-runtime/dist/esm/routes.js
170 | @@ -10,10 +10,6 @@
171 | */
172 | import { callRouteLoaderRR, callRouteActionRR } from './data.js';
173 |
174 | -// NOTE: make sure to change the Route in remix-react if you change this
175 | -
176 | -// NOTE: make sure to change the EntryRoute in remix-react if you change this
177 | -
178 | function groupRoutesByParentId(manifest) {
179 | let routes = {};
180 | Object.values(manifest).forEach(route => {
181 | diff --git a/node_modules/@remix-run/server-runtime/dist/esm/server.js b/node_modules/@remix-run/server-runtime/dist/esm/server.js
182 | index 934aac5..0ea7569 100644
183 | --- a/node_modules/@remix-run/server-runtime/dist/esm/server.js
184 | +++ b/node_modules/@remix-run/server-runtime/dist/esm/server.js
185 | @@ -14,9 +14,9 @@ import { serializeError, sanitizeErrors, serializeErrors } from './errors.js';
186 | import { getDocumentHeadersRR } from './headers.js';
187 | import invariant from './invariant.js';
188 | import { isServerMode, ServerMode } from './mode.js';
189 | +import { isRedirectResponse, createDeferredReadableStream, isResponse, json } from './responses.js';
190 | import { matchServerRoutes } from './routeMatching.js';
191 | import { createRoutes, createStaticHandlerDataRoutes } from './routes.js';
192 | -import { isRedirectResponse, createDeferredReadableStream, isResponse, json } from './responses.js';
193 | import { createServerHandoffString } from './serverHandoff.js';
194 |
195 | const createRequestHandler = (build, mode) => {
196 | @@ -24,13 +24,25 @@ const createRequestHandler = (build, mode) => {
197 | let dataRoutes = createStaticHandlerDataRoutes(build.routes, build.future);
198 | let serverMode = isServerMode(mode) ? mode : ServerMode.Production;
199 | let staticHandler = createStaticHandler(dataRoutes);
200 | + let errorHandler = build.entry.module.handleError || ((error, {
201 | + request
202 | + }) => {
203 | + if (serverMode !== ServerMode.Test && !request.signal.aborted) {
204 | + console.error(error);
205 | + }
206 | + });
207 | return async function requestHandler(request, loadContext = {}) {
208 | let url = new URL(request.url);
209 | let matches = matchServerRoutes(routes, url.pathname);
210 | + let handleError = error => errorHandler(error, {
211 | + context: loadContext,
212 | + params: matches && matches.length > 0 ? matches[0].params : {},
213 | + request
214 | + });
215 | let response;
216 | if (url.searchParams.has("_data")) {
217 | let routeId = url.searchParams.get("_data");
218 | - response = await handleDataRequestRR(serverMode, staticHandler, routeId, request, loadContext);
219 | + response = await handleDataRequestRR(serverMode, staticHandler, routeId, request, loadContext, handleError);
220 | if (build.entry.module.handleDataRequest) {
221 | let match = matches.find(match => match.route.id == routeId);
222 | response = await build.entry.module.handleDataRequest(response, {
223 | @@ -40,9 +52,9 @@ const createRequestHandler = (build, mode) => {
224 | });
225 | }
226 | } else if (matches && matches[matches.length - 1].route.module.default == null) {
227 | - response = await handleResourceRequestRR(serverMode, staticHandler, matches.slice(-1)[0].route.id, request, loadContext);
228 | + response = await handleResourceRequestRR(serverMode, staticHandler, matches.slice(-1)[0].route.id, request, loadContext, handleError);
229 | } else {
230 | - response = await handleDocumentRequestRR(serverMode, build, staticHandler, request, loadContext);
231 | + response = await handleDocumentRequestRR(serverMode, build, staticHandler, request, loadContext, handleError);
232 | }
233 | if (request.method === "HEAD") {
234 | return new Response(null, {
235 | @@ -54,7 +66,7 @@ const createRequestHandler = (build, mode) => {
236 | return response;
237 | };
238 | };
239 | -async function handleDataRequestRR(serverMode, staticHandler, routeId, request, loadContext) {
240 | +async function handleDataRequestRR(serverMode, staticHandler, routeId, request, loadContext, handleError) {
241 | try {
242 | let response = await staticHandler.queryRoute(request, {
243 | routeId,
244 | @@ -91,11 +103,14 @@ async function handleDataRequestRR(serverMode, staticHandler, routeId, request,
245 | error.headers.set("X-Remix-Catch", "yes");
246 | return error;
247 | }
248 | - let status = isRouteErrorResponse(error) ? error.status : 500;
249 | - let errorInstance = isRouteErrorResponse(error) && error.error ? error.error : error instanceof Error ? error : new Error("Unexpected Server Error");
250 | - logServerErrorIfNotAborted(errorInstance, request, serverMode);
251 | + if (isRouteErrorResponse(error)) {
252 | + handleError(error.error || error);
253 | + return errorResponseToJson(error, serverMode);
254 | + }
255 | + let errorInstance = error instanceof Error ? error : new Error("Unexpected Server Error");
256 | + handleError(errorInstance);
257 | return json(serializeError(errorInstance, serverMode), {
258 | - status,
259 | + status: 500,
260 | headers: {
261 | "X-Remix-Error": "yes"
262 | }
263 | @@ -136,14 +151,14 @@ function differentiateCatchVersusErrorBoundaries(build, context) {
264 | }
265 | context.errors = errors;
266 | }
267 | -async function handleDocumentRequestRR(serverMode, build, staticHandler, request, loadContext) {
268 | +async function handleDocumentRequestRR(serverMode, build, staticHandler, request, loadContext, handleError) {
269 | let context;
270 | try {
271 | context = await staticHandler.query(request, {
272 | requestContext: loadContext
273 | });
274 | } catch (error) {
275 | - logServerErrorIfNotAborted(error, request, serverMode);
276 | + handleError(error);
277 | return new Response(null, {
278 | status: 500
279 | });
280 | @@ -154,6 +169,7 @@ async function handleDocumentRequestRR(serverMode, build, staticHandler, request
281 |
282 | // Sanitize errors outside of development environments
283 | if (context.errors) {
284 | + Object.values(context.errors).forEach(err => handleError(err));
285 | context.errors = sanitizeErrors(context.errors, serverMode);
286 | }
287 |
288 | @@ -181,6 +197,7 @@ async function handleDocumentRequestRR(serverMode, build, staticHandler, request
289 | try {
290 | return await handleDocumentRequestFunction(request, context.statusCode, headers, entryContext, loadContext);
291 | } catch (error) {
292 | + handleError(error);
293 | // Get a new StaticHandlerContext that contains the error at the right boundary
294 | context = getStaticContextFromError(staticHandler.dataRoutes, context, error);
295 |
296 | @@ -210,12 +227,12 @@ async function handleDocumentRequestRR(serverMode, build, staticHandler, request
297 | try {
298 | return await handleDocumentRequestFunction(request, context.statusCode, headers, entryContext, loadContext);
299 | } catch (error) {
300 | - logServerErrorIfNotAborted(error, request, serverMode);
301 | + handleError(error);
302 | return returnLastResortErrorResponse(error, serverMode);
303 | }
304 | }
305 | }
306 | -async function handleResourceRequestRR(serverMode, staticHandler, routeId, request, loadContext) {
307 | +async function handleResourceRequestRR(serverMode, staticHandler, routeId, request, loadContext, handleError) {
308 | try {
309 | // Note we keep the routeId here to align with the Remix handling of
310 | // resource routes which doesn't take ?index into account and just takes
311 | @@ -234,14 +251,22 @@ async function handleResourceRequestRR(serverMode, staticHandler, routeId, reque
312 | error.headers.set("X-Remix-Catch", "yes");
313 | return error;
314 | }
315 | - logServerErrorIfNotAborted(error, request, serverMode);
316 | + if (isRouteErrorResponse(error)) {
317 | + handleError(error.error || error);
318 | + return errorResponseToJson(error, serverMode);
319 | + }
320 | + handleError(error);
321 | return returnLastResortErrorResponse(error, serverMode);
322 | }
323 | }
324 | -function logServerErrorIfNotAborted(error, request, serverMode) {
325 | - if (serverMode !== ServerMode.Test && !request.signal.aborted) {
326 | - console.error(error);
327 | - }
328 | +function errorResponseToJson(errorResponse, serverMode) {
329 | + return json(serializeError(errorResponse.error || new Error("Unexpected Server Error"), serverMode), {
330 | + status: errorResponse.status,
331 | + statusText: errorResponse.statusText,
332 | + headers: {
333 | + "X-Remix-Error": "yes"
334 | + }
335 | + });
336 | }
337 | function returnLastResortErrorResponse(error, serverMode) {
338 | let message = "Unexpected Server Error";
339 | diff --git a/node_modules/@remix-run/server-runtime/dist/esm/sessions.js b/node_modules/@remix-run/server-runtime/dist/esm/sessions.js
340 | index 0968452..2b31ef1 100644
341 | --- a/node_modules/@remix-run/server-runtime/dist/esm/sessions.js
342 | +++ b/node_modules/@remix-run/server-runtime/dist/esm/sessions.js
343 | @@ -15,12 +15,6 @@ import { warnOnce } from './warnings.js';
344 | * An object of name/value pairs to be used in the session.
345 | */
346 |
347 | -/**
348 | - * Session persists data across HTTP requests.
349 | - *
350 | - * @see https://remix.run/utils/sessions#session-api
351 | - */
352 | -
353 | function flash(name) {
354 | return `__flash_${name}__`;
355 | }
356 | @@ -82,16 +76,6 @@ const isSession = object => {
357 | * Then, later it generates the `Set-Cookie` header to be used in the response.
358 | */
359 |
360 | -/**
361 | - * SessionIdStorageStrategy is designed to allow anyone to easily build their
362 | - * own SessionStorage using `createSessionStorage(strategy)`.
363 | - *
364 | - * This strategy describes a common scenario where the session id is stored in
365 | - * a cookie but the actual session data is stored elsewhere, usually in a
366 | - * database or on disk. A set of create, read, update, and delete operations
367 | - * are provided for managing the session data.
368 | - */
369 | -
370 | /**
371 | * Creates a SessionStorage object using a SessionIdStorageStrategy.
372 | *
373 | diff --git a/node_modules/@remix-run/server-runtime/dist/mode.js b/node_modules/@remix-run/server-runtime/dist/mode.js
374 | index 15c67c6..3fa80fd 100644
375 | --- a/node_modules/@remix-run/server-runtime/dist/mode.js
376 | +++ b/node_modules/@remix-run/server-runtime/dist/mode.js
377 | @@ -15,15 +15,14 @@ Object.defineProperty(exports, '__esModule', { value: true });
378 | /**
379 | * The mode to use when running the server.
380 | */
381 | -let ServerMode = /*#__PURE__*/function (ServerMode) {
382 | +exports.ServerMode = void 0;
383 | +(function (ServerMode) {
384 | ServerMode["Development"] = "development";
385 | ServerMode["Production"] = "production";
386 | ServerMode["Test"] = "test";
387 | - return ServerMode;
388 | -}({});
389 | +})(exports.ServerMode || (exports.ServerMode = {}));
390 | function isServerMode(value) {
391 | - return value === ServerMode.Development || value === ServerMode.Production || value === ServerMode.Test;
392 | + return value === exports.ServerMode.Development || value === exports.ServerMode.Production || value === exports.ServerMode.Test;
393 | }
394 |
395 | -exports.ServerMode = ServerMode;
396 | exports.isServerMode = isServerMode;
397 | diff --git a/node_modules/@remix-run/server-runtime/dist/responses.js b/node_modules/@remix-run/server-runtime/dist/responses.js
398 | index ee5d137..cb16d3e 100644
399 | --- a/node_modules/@remix-run/server-runtime/dist/responses.js
400 | +++ b/node_modules/@remix-run/server-runtime/dist/responses.js
401 | @@ -15,8 +15,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
402 | var router = require('@remix-run/router');
403 | var errors = require('./errors.js');
404 |
405 | -// must be a type since this is a subtype of response
406 | -// interfaces must conform to the types they extend
407 | /**
408 | * This is a shortcut for creating `application/json` responses. Converts `data`
409 | * to JSON and sets the `Content-Type` header.
410 | diff --git a/node_modules/@remix-run/server-runtime/dist/routes.js b/node_modules/@remix-run/server-runtime/dist/routes.js
411 | index 8c97233..188134f 100644
412 | --- a/node_modules/@remix-run/server-runtime/dist/routes.js
413 | +++ b/node_modules/@remix-run/server-runtime/dist/routes.js
414 | @@ -14,10 +14,6 @@ Object.defineProperty(exports, '__esModule', { value: true });
415 |
416 | var data = require('./data.js');
417 |
418 | -// NOTE: make sure to change the Route in remix-react if you change this
419 | -
420 | -// NOTE: make sure to change the EntryRoute in remix-react if you change this
421 | -
422 | function groupRoutesByParentId(manifest) {
423 | let routes = {};
424 | Object.values(manifest).forEach(route => {
425 | diff --git a/node_modules/@remix-run/server-runtime/dist/server.js b/node_modules/@remix-run/server-runtime/dist/server.js
426 | index 2219e38..9a02306 100644
427 | --- a/node_modules/@remix-run/server-runtime/dist/server.js
428 | +++ b/node_modules/@remix-run/server-runtime/dist/server.js
429 | @@ -18,9 +18,9 @@ var errors = require('./errors.js');
430 | var headers = require('./headers.js');
431 | var invariant = require('./invariant.js');
432 | var mode = require('./mode.js');
433 | +var responses = require('./responses.js');
434 | var routeMatching = require('./routeMatching.js');
435 | var routes = require('./routes.js');
436 | -var responses = require('./responses.js');
437 | var serverHandoff = require('./serverHandoff.js');
438 |
439 | const createRequestHandler = (build, mode$1) => {
440 | @@ -28,13 +28,25 @@ const createRequestHandler = (build, mode$1) => {
441 | let dataRoutes = routes.createStaticHandlerDataRoutes(build.routes, build.future);
442 | let serverMode = mode.isServerMode(mode$1) ? mode$1 : mode.ServerMode.Production;
443 | let staticHandler = router.createStaticHandler(dataRoutes);
444 | + let errorHandler = build.entry.module.handleError || ((error, {
445 | + request
446 | + }) => {
447 | + if (serverMode !== mode.ServerMode.Test && !request.signal.aborted) {
448 | + console.error(error);
449 | + }
450 | + });
451 | return async function requestHandler(request, loadContext = {}) {
452 | let url = new URL(request.url);
453 | let matches = routeMatching.matchServerRoutes(routes$1, url.pathname);
454 | + let handleError = error => errorHandler(error, {
455 | + context: loadContext,
456 | + params: matches && matches.length > 0 ? matches[0].params : {},
457 | + request
458 | + });
459 | let response;
460 | if (url.searchParams.has("_data")) {
461 | let routeId = url.searchParams.get("_data");
462 | - response = await handleDataRequestRR(serverMode, staticHandler, routeId, request, loadContext);
463 | + response = await handleDataRequestRR(serverMode, staticHandler, routeId, request, loadContext, handleError);
464 | if (build.entry.module.handleDataRequest) {
465 | let match = matches.find(match => match.route.id == routeId);
466 | response = await build.entry.module.handleDataRequest(response, {
467 | @@ -44,9 +56,9 @@ const createRequestHandler = (build, mode$1) => {
468 | });
469 | }
470 | } else if (matches && matches[matches.length - 1].route.module.default == null) {
471 | - response = await handleResourceRequestRR(serverMode, staticHandler, matches.slice(-1)[0].route.id, request, loadContext);
472 | + response = await handleResourceRequestRR(serverMode, staticHandler, matches.slice(-1)[0].route.id, request, loadContext, handleError);
473 | } else {
474 | - response = await handleDocumentRequestRR(serverMode, build, staticHandler, request, loadContext);
475 | + response = await handleDocumentRequestRR(serverMode, build, staticHandler, request, loadContext, handleError);
476 | }
477 | if (request.method === "HEAD") {
478 | return new Response(null, {
479 | @@ -58,7 +70,7 @@ const createRequestHandler = (build, mode$1) => {
480 | return response;
481 | };
482 | };
483 | -async function handleDataRequestRR(serverMode, staticHandler, routeId, request, loadContext) {
484 | +async function handleDataRequestRR(serverMode, staticHandler, routeId, request, loadContext, handleError) {
485 | try {
486 | let response = await staticHandler.queryRoute(request, {
487 | routeId,
488 | @@ -95,11 +107,14 @@ async function handleDataRequestRR(serverMode, staticHandler, routeId, request,
489 | error.headers.set("X-Remix-Catch", "yes");
490 | return error;
491 | }
492 | - let status = router.isRouteErrorResponse(error) ? error.status : 500;
493 | - let errorInstance = router.isRouteErrorResponse(error) && error.error ? error.error : error instanceof Error ? error : new Error("Unexpected Server Error");
494 | - logServerErrorIfNotAborted(errorInstance, request, serverMode);
495 | + if (router.isRouteErrorResponse(error)) {
496 | + handleError(error.error || error);
497 | + return errorResponseToJson(error, serverMode);
498 | + }
499 | + let errorInstance = error instanceof Error ? error : new Error("Unexpected Server Error");
500 | + handleError(errorInstance);
501 | return responses.json(errors.serializeError(errorInstance, serverMode), {
502 | - status,
503 | + status: 500,
504 | headers: {
505 | "X-Remix-Error": "yes"
506 | }
507 | @@ -140,14 +155,14 @@ function differentiateCatchVersusErrorBoundaries(build, context) {
508 | }
509 | context.errors = errors;
510 | }
511 | -async function handleDocumentRequestRR(serverMode, build, staticHandler, request, loadContext) {
512 | +async function handleDocumentRequestRR(serverMode, build, staticHandler, request, loadContext, handleError) {
513 | let context;
514 | try {
515 | context = await staticHandler.query(request, {
516 | requestContext: loadContext
517 | });
518 | } catch (error) {
519 | - logServerErrorIfNotAborted(error, request, serverMode);
520 | + handleError(error);
521 | return new Response(null, {
522 | status: 500
523 | });
524 | @@ -158,6 +173,7 @@ async function handleDocumentRequestRR(serverMode, build, staticHandler, request
525 |
526 | // Sanitize errors outside of development environments
527 | if (context.errors) {
528 | + Object.values(context.errors).forEach(err => handleError(err));
529 | context.errors = errors.sanitizeErrors(context.errors, serverMode);
530 | }
531 |
532 | @@ -185,6 +201,7 @@ async function handleDocumentRequestRR(serverMode, build, staticHandler, request
533 | try {
534 | return await handleDocumentRequestFunction(request, context.statusCode, headers$1, entryContext, loadContext);
535 | } catch (error) {
536 | + handleError(error);
537 | // Get a new StaticHandlerContext that contains the error at the right boundary
538 | context = router.getStaticContextFromError(staticHandler.dataRoutes, context, error);
539 |
540 | @@ -214,12 +231,12 @@ async function handleDocumentRequestRR(serverMode, build, staticHandler, request
541 | try {
542 | return await handleDocumentRequestFunction(request, context.statusCode, headers$1, entryContext, loadContext);
543 | } catch (error) {
544 | - logServerErrorIfNotAborted(error, request, serverMode);
545 | + handleError(error);
546 | return returnLastResortErrorResponse(error, serverMode);
547 | }
548 | }
549 | }
550 | -async function handleResourceRequestRR(serverMode, staticHandler, routeId, request, loadContext) {
551 | +async function handleResourceRequestRR(serverMode, staticHandler, routeId, request, loadContext, handleError) {
552 | try {
553 | // Note we keep the routeId here to align with the Remix handling of
554 | // resource routes which doesn't take ?index into account and just takes
555 | @@ -238,14 +255,22 @@ async function handleResourceRequestRR(serverMode, staticHandler, routeId, reque
556 | error.headers.set("X-Remix-Catch", "yes");
557 | return error;
558 | }
559 | - logServerErrorIfNotAborted(error, request, serverMode);
560 | + if (router.isRouteErrorResponse(error)) {
561 | + handleError(error.error || error);
562 | + return errorResponseToJson(error, serverMode);
563 | + }
564 | + handleError(error);
565 | return returnLastResortErrorResponse(error, serverMode);
566 | }
567 | }
568 | -function logServerErrorIfNotAborted(error, request, serverMode) {
569 | - if (serverMode !== mode.ServerMode.Test && !request.signal.aborted) {
570 | - console.error(error);
571 | - }
572 | +function errorResponseToJson(errorResponse, serverMode) {
573 | + return responses.json(errors.serializeError(errorResponse.error || new Error("Unexpected Server Error"), serverMode), {
574 | + status: errorResponse.status,
575 | + statusText: errorResponse.statusText,
576 | + headers: {
577 | + "X-Remix-Error": "yes"
578 | + }
579 | + });
580 | }
581 | function returnLastResortErrorResponse(error, serverMode) {
582 | let message = "Unexpected Server Error";
583 | diff --git a/node_modules/@remix-run/server-runtime/dist/sessions.js b/node_modules/@remix-run/server-runtime/dist/sessions.js
584 | index e56b3cc..2440449 100644
585 | --- a/node_modules/@remix-run/server-runtime/dist/sessions.js
586 | +++ b/node_modules/@remix-run/server-runtime/dist/sessions.js
587 | @@ -19,12 +19,6 @@ var warnings = require('./warnings.js');
588 | * An object of name/value pairs to be used in the session.
589 | */
590 |
591 | -/**
592 | - * Session persists data across HTTP requests.
593 | - *
594 | - * @see https://remix.run/utils/sessions#session-api
595 | - */
596 | -
597 | function flash(name) {
598 | return `__flash_${name}__`;
599 | }
600 | @@ -86,16 +80,6 @@ const isSession = object => {
601 | * Then, later it generates the `Set-Cookie` header to be used in the response.
602 | */
603 |
604 | -/**
605 | - * SessionIdStorageStrategy is designed to allow anyone to easily build their
606 | - * own SessionStorage using `createSessionStorage(strategy)`.
607 | - *
608 | - * This strategy describes a common scenario where the session id is stored in
609 | - * a cookie but the actual session data is stored elsewhere, usually in a
610 | - * database or on disk. A set of create, read, update, and delete operations
611 | - * are provided for managing the session data.
612 | - */
613 | -
614 | /**
615 | * Creates a SessionStorage object using a SessionIdStorageStrategy.
616 | *
617 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kiliman/remix-error-logging/898365467c805fdd8aeaef2ae5d6efc9a348c4cd/public/favicon.ico
--------------------------------------------------------------------------------
/remix.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('@remix-run/dev').AppConfig} */
2 | module.exports = {
3 | ignoredRouteFiles: ['**/.*'],
4 | // appDirectory: "app",
5 | // assetsBuildDirectory: "public/build",
6 | // serverBuildPath: "build/index.js",
7 | // publicPath: "/build/",
8 | serverModuleFormat: 'cjs',
9 | future: {
10 | v2_errorBoundary: true,
11 | v2_meta: true,
12 | v2_normalizeFormMethod: true,
13 | v2_routeConvention: true,
14 | },
15 | }
16 |
--------------------------------------------------------------------------------
/remix.env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3 | "compilerOptions": {
4 | "lib": ["DOM", "DOM.Iterable", "ES2019"],
5 | "isolatedModules": true,
6 | "esModuleInterop": true,
7 | "jsx": "react-jsx",
8 | "moduleResolution": "node",
9 | "resolveJsonModule": true,
10 | "target": "ES2019",
11 | "strict": true,
12 | "allowJs": true,
13 | "forceConsistentCasingInFileNames": true,
14 | "baseUrl": ".",
15 | "paths": {
16 | "~/*": ["./app/*"]
17 | },
18 |
19 | // Remix takes care of building everything in `remix build`.
20 | "noEmit": true
21 | }
22 | }
23 |
--------------------------------------------------------------------------------