├── .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 |
    16 |
  • 17 | Bug 18 |
  • 19 |
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 | --------------------------------------------------------------------------------