Skip to content

Commit 6cc15e4

Browse files
committed
Switch errors to .message not .error
1 parent 9c1791d commit 6cc15e4

6 files changed

Lines changed: 97 additions & 44 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ All error responses follow this format:
528528
```json
529529
{
530530
"code": "ERROR_CODE",
531-
"error": "Human-readable error message"
531+
"message": "Human-readable error message"
532532
}
533533
```
534534

src/lib/download-request.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { StatusError } from 'itty-router'
21
import { Agent as SecretStreamAgent } from 'secret-stream-http'
32
import z32 from 'z32'
43

54
import { TypedEventTarget } from '../lib/event-target.js'
65
import type { DownloadCreateRequest } from '../routes/downloads.js'
76
import { type DownloadStateUpdate } from '../types.js'
7+
import { StatusError } from './errors.js'
88
import { errors, jsonError } from './errors.js'
99
import { secretStreamFetch } from './secret-stream-fetch.js'
1010
import { StateUpdateEvent } from './state-update-event.js'

src/lib/errors.ts

Lines changed: 80 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,99 +1,121 @@
1-
import { StatusError } from 'itty-router'
1+
import { json, type ErrorFormatter } from 'itty-router'
2+
3+
interface ErrorDefinition {
4+
message: string
5+
status: number
6+
code: string
7+
}
8+
9+
type StatusErrorObject = {
10+
message?: string
11+
[key: string]: any
12+
}
13+
14+
export class StatusError extends Error {
15+
status: number;
16+
[key: string]: any
17+
18+
constructor(status = 500, body?: StatusErrorObject | string) {
19+
super(typeof body === 'object' ? body.message : body)
20+
if (typeof body === 'object') Object.assign(this, body)
21+
this.status = status
22+
}
23+
}
224

325
const errorsList = [
426
// Download errors (receiver-side)
527
{
628
code: 'DOWNLOAD_NOT_FOUND',
7-
error: 'Download not found',
29+
message: 'Download not found',
830
status: 404,
931
},
1032
{
1133
code: 'DOWNLOAD_ERROR',
12-
error: 'Download failed',
34+
message: 'Download failed',
1335
status: 500,
1436
},
1537
{
1638
code: 'DOWNLOAD_SHARE_CANCELED',
17-
error: 'Download canceled by sender',
39+
message: 'Download canceled by sender',
1840
status: 409,
1941
},
2042
{
2143
code: 'DOWNLOAD_SHARE_DECLINED',
22-
error: 'Cannot download: share was declined',
44+
message: 'Cannot download: share was declined',
2345
status: 409,
2446
},
2547
{
2648
code: 'DOWNLOAD_SHARE_NOT_PENDING',
27-
error: 'Cannot download: share is not pending',
49+
message: 'Cannot download: share is not pending',
2850
status: 409,
2951
},
3052
{
3153
code: 'ABORT_NOT_DOWNLOADING',
32-
error: 'Cannot abort: download is not in progress',
54+
message: 'Cannot abort: download is not in progress',
3355
status: 409,
3456
},
3557
{
3658
code: 'INVALID_SENDER_DEVICE_ID',
37-
error: 'Invalid sender device ID',
59+
message: 'Invalid sender device ID',
3860
status: 400,
3961
},
4062

4163
// Map share errors (sender-side)
4264
{
4365
code: 'MAP_SHARE_NOT_FOUND',
44-
error: 'Map share not found',
66+
message: 'Map share not found',
4567
status: 404,
4668
},
4769
{
4870
code: 'CANCEL_SHARE_NOT_CANCELABLE',
49-
error: 'Cannot cancel: share is not pending or downloading',
71+
message: 'Cannot cancel: share is not pending or downloading',
5072
status: 409,
5173
},
5274
{
5375
code: 'DECLINE_SHARE_NOT_PENDING',
54-
error: 'Cannot decline: share is not pending',
76+
message: 'Cannot decline: share is not pending',
5577
status: 409,
5678
},
5779
{
5880
code: 'DECLINE_CANNOT_CONNECT',
59-
error: 'Cannot decline: unable to connect to sender',
81+
message: 'Cannot decline: unable to connect to sender',
6082
status: 502,
6183
},
6284

6385
// Map errors
6486
{
6587
code: 'MAP_NOT_FOUND',
66-
error: 'Map not found',
88+
message: 'Map not found',
6789
status: 404,
6890
},
6991
{
7092
code: 'INVALID_MAP_FILE',
71-
error: 'Invalid map file',
93+
message: 'Invalid map file',
7294
status: 400,
7395
},
7496

7597
// Generic errors
7698
{
7799
code: 'FORBIDDEN',
78-
error: 'Forbidden',
100+
message: 'Forbidden',
79101
status: 403,
80102
},
81103
{
82104
code: 'INVALID_REQUEST',
83-
error: 'Invalid request',
105+
message: 'Invalid request',
84106
status: 400,
85107
},
86-
] as const satisfies Array<{ error: string; status: number; code: string }>
108+
] as const satisfies Array<ErrorDefinition>
87109

88110
export const errors = {} as Record<
89111
(typeof errorsList)[number]['code'],
90112
new (body?: { [key: string]: any } | string) => StatusError
91113
>
92-
for (const { code, error, status } of errorsList) {
114+
for (const { code, message, status } of errorsList) {
93115
errors[code] = class extends StatusError {
94116
constructor(body?: { [key: string]: any } | string) {
95-
body = typeof body === 'string' ? { error: body } : body
96-
super(status, { code, error, ...body })
117+
body = typeof body === 'string' ? { message: body } : body
118+
super(status, { code, message, ...body })
97119
}
98120
}
99121
}
@@ -120,3 +142,41 @@ export function jsonError(err: unknown): { message: string; code: string } {
120142
}
121143
}
122144
}
145+
146+
const getMessage = (code: number): string =>
147+
({
148+
400: 'Bad Request',
149+
401: 'Unauthorized',
150+
403: 'Forbidden',
151+
404: 'Not Found',
152+
500: 'Internal Server Error',
153+
})[code] || 'Unknown Error'
154+
155+
const getCode = (status: number): string =>
156+
({
157+
400: 'BAD_REQUEST',
158+
401: 'UNAUTHORIZED',
159+
403: 'FORBIDDEN',
160+
404: 'NOT_FOUND',
161+
500: 'INTERNAL_SERVER_ERROR',
162+
})[status] || 'UNKNOWN_ERROR'
163+
164+
export const error: ErrorFormatter = (a = 500, b?) => {
165+
// handle passing an Error | StatusError directly in
166+
if (a instanceof Error) {
167+
const { message, code, ...err } = a
168+
a = a.status || 500
169+
b = {
170+
message: message || getMessage(a),
171+
code: code || getCode(a),
172+
...err,
173+
}
174+
}
175+
176+
b = {
177+
status: a,
178+
...(typeof b === 'object' ? b : { message: b || getMessage(a) }),
179+
}
180+
181+
return json(b, { status: a })
182+
}

src/routes/map-shares.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
import os from 'node:os'
22

3-
import {
4-
IRequestStrict,
5-
IttyRouter,
6-
StatusError,
7-
type RequestHandler,
8-
} from 'itty-router'
3+
import { IRequestStrict, IttyRouter, type RequestHandler } from 'itty-router'
94
import {
105
fetch as secretStreamFetch,
116
Agent as SecretStreamAgent,
@@ -15,7 +10,7 @@ import { Compile } from 'typebox/compile'
1510
import z32 from 'z32'
1611

1712
import type { Context } from '../context.js'
18-
import { errors } from '../lib/errors.js'
13+
import { errors, StatusError } from '../lib/errors.js'
1914
import { createEventStreamResponse } from '../lib/event-stream-response.js'
2015
import { MapShare } from '../lib/map-share.js'
2116
import { SelfEvictingTimeoutMap } from '../lib/self-evicting-map.js'

src/routes/root.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { error, json, Router, type IRequestStrict } from 'itty-router'
1+
import { json, Router, type IRequestStrict } from 'itty-router'
22

33
import type { Context } from '../context.js'
4+
import { error } from '../lib/errors.js'
45
import { localhostOnly } from '../middlewares/localhost-only.js'
56
import type { FetchContext, RouterExternal } from '../types.js'
67
import { DownloadsRouter } from './downloads.js'
@@ -16,10 +17,7 @@ export function RootRouter({ base = '/' }, ctx: Context): RouterExternal {
1617
base,
1718
// The `error` handler will send a response with the status code from any
1819
// thrown StatusError, or a 500 for any other errors.
19-
catch: (err) => {
20-
// console.error(err)
21-
return error(err)
22-
},
20+
catch: (err) => error(err),
2321
// Sends a 404 response for any requests that don't match a route, and for
2422
// any request handlers that return JSON will send a JSON response.
2523
finally: [(response) => response ?? error(404), json],

test/map-shares.test.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ describe('Map Shares and Downloads', () => {
7575
expect(response.status).toBe(404)
7676
const body = await response.json()
7777
expect(body).toHaveProperty('code', 'MAP_SHARE_NOT_FOUND')
78-
expect(body).toHaveProperty('error')
78+
expect(body).toHaveProperty('message')
7979
})
8080

8181
it('should return 404 for events on non-existent share', async (t) => {
@@ -99,7 +99,7 @@ describe('Map Shares and Downloads', () => {
9999
expect(response.status).toBe(404)
100100
const body = await response.json()
101101
expect(body).toHaveProperty('code', 'MAP_NOT_FOUND')
102-
expect(body).toHaveProperty('error')
102+
expect(body).toHaveProperty('message')
103103
})
104104

105105
it('should allow creating multiple shares for the same receiver', async (t) => {
@@ -220,7 +220,7 @@ describe('Map Shares and Downloads', () => {
220220
expect(response.status).toBe(404)
221221
const body = await response.json()
222222
expect(body).toHaveProperty('code', 'DOWNLOAD_NOT_FOUND')
223-
expect(body).toHaveProperty('error')
223+
expect(body).toHaveProperty('message')
224224
})
225225

226226
it('should return 404 for events on non-existent download', async (t) => {
@@ -552,7 +552,7 @@ describe('Map Shares and Downloads', () => {
552552
expect(declineResponse.status).toBe(502)
553553
const body = await declineResponse.json()
554554
expect(body).toHaveProperty('code', 'DECLINE_CANNOT_CONNECT')
555-
expect(body).toHaveProperty('error')
555+
expect(body).toHaveProperty('message')
556556
})
557557
})
558558

@@ -659,7 +659,7 @@ describe('Map Shares and Downloads', () => {
659659
expect(cancelResponse.status).toBe(409)
660660
const body = await cancelResponse.json()
661661
expect(body).toHaveProperty('code', 'ABORT_NOT_DOWNLOADING')
662-
expect(body).toHaveProperty('error')
662+
expect(body).toHaveProperty('message')
663663
})
664664

665665
it('should preserve existing map when receiver aborts download', async (t) => {
@@ -1219,7 +1219,7 @@ describe('Map Shares and Downloads', () => {
12191219
expect(response.status).toBe(400)
12201220
const body = await response.json()
12211221
expect(body).toHaveProperty('code', 'INVALID_REQUEST')
1222-
expect(body).toHaveProperty('error')
1222+
expect(body).toHaveProperty('message')
12231223
}
12241224
})
12251225

@@ -1287,7 +1287,7 @@ describe('Map Shares and Downloads', () => {
12871287
expect(response.status).toBe(400)
12881288
const body = await response.json()
12891289
expect(body).toHaveProperty('code', 'INVALID_REQUEST')
1290-
expect(body).toHaveProperty('error')
1290+
expect(body).toHaveProperty('message')
12911291
}
12921292
})
12931293
})
@@ -1379,7 +1379,7 @@ describe('Map Shares and Downloads', () => {
13791379
expect(response.status).toBe(400)
13801380
const body = await response.json()
13811381
expect(body).toHaveProperty('code', 'INVALID_REQUEST')
1382-
expect(body).toHaveProperty('error')
1382+
expect(body).toHaveProperty('message')
13831383
}
13841384
})
13851385

@@ -1431,7 +1431,7 @@ describe('Map Shares and Downloads', () => {
14311431
expect(response.status).toBe(404)
14321432
const body = await response.json()
14331433
expect(body).toHaveProperty('code', 'MAP_NOT_FOUND')
1434-
expect(body).toHaveProperty('error')
1434+
expect(body).toHaveProperty('message')
14351435
})
14361436

14371437
it('should reject PUT with empty body', async (t) => {
@@ -1444,7 +1444,7 @@ describe('Map Shares and Downloads', () => {
14441444
const body = await response.json()
14451445
// Empty body reaches map validation which returns INVALID_MAP_FILE
14461446
expect(body).toHaveProperty('code', 'INVALID_MAP_FILE')
1447-
expect(body).toHaveProperty('error')
1447+
expect(body).toHaveProperty('message')
14481448
})
14491449

14501450
it('should reject DELETE of non-custom map', async (t) => {

0 commit comments

Comments
 (0)