Skip to content

Commit 0fc5c71

Browse files
committed
feat: enhance asset HTTP server with improved error handling and logging
1 parent b287a80 commit 0fc5c71

File tree

1 file changed

+66
-30
lines changed

1 file changed

+66
-30
lines changed

packages/mcp-server/src/asset-http-server.ts

Lines changed: 66 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { MCP_HASH_PATTERN } from '@tempad-dev/mcp-shared'
1+
import { MCP_HASH_HEX_LENGTH, MCP_HASH_PATTERN } from '@tempad-dev/mcp-shared'
22
import { nanoid } from 'nanoid'
33
import { createHash } from 'node:crypto'
44
import {
@@ -68,6 +68,19 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
6868
}
6969

7070
function handleRequest(req: IncomingMessage, res: ServerResponse): void {
71+
const startedAt = Date.now()
72+
res.on('finish', () => {
73+
log.info(
74+
{
75+
method: req.method,
76+
url: req.url,
77+
status: res.statusCode,
78+
durationMs: Date.now() - startedAt
79+
},
80+
'HTTP asset request completed.'
81+
)
82+
})
83+
7184
res.setHeader('Access-Control-Allow-Origin', '*')
7285
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS')
7386
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, X-Asset-Width, X-Asset-Height')
@@ -79,16 +92,14 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
7992
}
8093

8194
if (!req.url) {
82-
res.writeHead(400)
83-
res.end('Missing URL')
95+
sendError(res, 400, 'Missing URL')
8496
return
8597
}
8698

8799
const url = new URL(req.url, getBaseUrl())
88100
const segments = url.pathname.split('/').filter(Boolean)
89101
if (segments.length !== 2 || segments[0] !== 'assets') {
90-
res.writeHead(404)
91-
res.end('Not Found')
102+
sendError(res, 404, 'Not Found')
92103
return
93104
}
94105

@@ -104,15 +115,13 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
104115
return
105116
}
106117

107-
res.writeHead(405)
108-
res.end('Method Not Allowed')
118+
sendError(res, 405, 'Method Not Allowed')
109119
}
110120

111121
function handleDownload(req: IncomingMessage, res: ServerResponse, hash: string): void {
112122
const record = store.get(hash)
113123
if (!record) {
114-
res.writeHead(404)
115-
res.end('Not Found')
124+
sendError(res, 404, 'Asset Not Found')
116125
return
117126
}
118127

@@ -123,12 +132,10 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
123132
const err = error as NodeJS.ErrnoException
124133
if (err.code === 'ENOENT') {
125134
store.remove(hash, { removeFile: false })
126-
res.writeHead(404)
127-
res.end('Not Found')
135+
sendError(res, 404, 'Asset Not Found')
128136
} else {
129137
log.error({ error, hash }, 'Failed to stat asset file.')
130-
res.writeHead(500)
131-
res.end('Internal Server Error')
138+
sendError(res, 500, 'Internal Server Error')
132139
}
133140
return
134141
}
@@ -143,9 +150,10 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
143150
stream.on('error', (error) => {
144151
log.warn({ error, hash }, 'Failed to stream asset file.')
145152
if (!res.headersSent) {
146-
res.writeHead(500)
153+
sendError(res, 500, 'Internal Server Error')
154+
} else {
155+
res.end()
147156
}
148-
res.end('Internal Server Error')
149157
})
150158
stream.on('open', () => {
151159
store.touch(hash)
@@ -155,8 +163,7 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
155163

156164
function handleUpload(req: IncomingMessage, res: ServerResponse, hash: string): void {
157165
if (!MCP_HASH_PATTERN.test(hash)) {
158-
res.writeHead(400)
159-
res.end('Invalid Hash Format')
166+
sendError(res, 400, 'Invalid Hash Format')
160167
return
161168
}
162169

@@ -187,8 +194,7 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
187194
store.upsert(existing)
188195
}
189196
store.touch(hash)
190-
res.writeHead(200)
191-
res.end('OK')
197+
sendOk(res, 200, 'Asset Already Exists')
192198
return
193199
}
194200

@@ -223,25 +229,23 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
223229
if (err) {
224230
cleanup()
225231
if (err.message === 'PayloadTooLarge') {
226-
res.writeHead(413)
227-
res.end('Payload Too Large')
232+
sendError(res, 413, 'Payload Too Large')
228233
} else if (err.code === 'ERR_STREAM_PREMATURE_CLOSE') {
229234
log.warn({ hash }, 'Upload request closed prematurely.')
235+
sendError(res, 400, 'Upload Incomplete')
230236
} else {
231237
log.error({ error: err, hash }, 'Upload pipeline failed.')
232238
if (!res.headersSent) {
233-
res.writeHead(500)
234-
res.end('Internal Server Error')
239+
sendError(res, 500, 'Internal Server Error')
235240
}
236241
}
237242
return
238243
}
239244

240-
const computedHash = hasher.digest('hex')
245+
const computedHash = hasher.digest('hex').slice(0, MCP_HASH_HEX_LENGTH)
241246
if (computedHash !== hash) {
242247
cleanup()
243-
res.writeHead(400)
244-
res.end('Hash Mismatch')
248+
sendError(res, 400, 'Hash Mismatch')
245249
return
246250
}
247251

@@ -250,8 +254,7 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
250254
} catch (error) {
251255
log.error({ error, hash }, 'Failed to rename temp file to asset.')
252256
cleanup()
253-
res.writeHead(500)
254-
res.end('Internal Server Error')
257+
sendError(res, 500, 'Internal Server Error')
255258
return
256259
}
257260

@@ -263,11 +266,44 @@ export function createAssetHttpServer(store: AssetStore): AssetHttpServer {
263266
metadata
264267
})
265268
log.info({ hash, size }, 'Stored uploaded asset via HTTP.')
266-
res.writeHead(201)
267-
res.end('Created')
269+
sendOk(res, 201, 'Created', { hash, size })
268270
})
269271
}
270272

273+
function sendError(
274+
res: ServerResponse,
275+
status: number,
276+
message: string,
277+
details?: Record<string, unknown>
278+
): void {
279+
if (!res.headersSent) {
280+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' })
281+
}
282+
res.end(
283+
JSON.stringify({
284+
error: message,
285+
...details
286+
})
287+
)
288+
}
289+
290+
function sendOk(
291+
res: ServerResponse,
292+
status: number,
293+
message: string,
294+
data?: Record<string, unknown>
295+
): void {
296+
if (!res.headersSent) {
297+
res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8' })
298+
}
299+
res.end(
300+
JSON.stringify({
301+
message,
302+
...data
303+
})
304+
)
305+
}
306+
271307
return {
272308
start,
273309
stop,

0 commit comments

Comments
 (0)