From e6b6739e8dd1fd801c34cc951cfe90951c2f9feb Mon Sep 17 00:00:00 2001 From: kptdobe Date: Wed, 29 Apr 2026 16:00:06 +0200 Subject: [PATCH] feat: add x-error header --- src/index.js | 2 +- src/storage/object/get.js | 10 ++++++++-- src/storage/object/move.js | 4 ++-- src/storage/version/audit.js | 2 +- src/storage/version/put.js | 8 ++++---- src/utils/daResp.js | 7 ++++++- test/storage/object/move.test.js | 2 ++ test/storage/version/audit.test.js | 2 ++ test/storage/version/put.test.js | 6 ++++++ test/utils/daResp.test.js | 13 ++++++++++--- 10 files changed, 42 insertions(+), 14 deletions(-) diff --git a/src/index.js b/src/index.js index c0f4a301..b35699b2 100644 --- a/src/index.js +++ b/src/index.js @@ -36,7 +36,7 @@ export default { return daResp({ status: 400 }); } console.error('Error computing context', e); - return daResp({ status: 500 }); + return daResp({ status: 500, error: e.message }); } const { users, authorized, key } = daCtx; diff --git a/src/storage/object/get.js b/src/storage/object/get.js index 8ca7e843..140f7945 100644 --- a/src/storage/object/get.js +++ b/src/storage/object/get.js @@ -89,7 +89,10 @@ export default async function getObject( etag: e.ETag || conditionalHeaders?.ifNoneMatch, }; } - return { body: '', status, contentLength: 0 }; + const error = status >= 500 ? e.message : undefined; + return { + body: '', status, contentLength: 0, ...(error ? { error } : {}), + }; } } // HEAD request path - uses presigned URL with fetch @@ -141,6 +144,9 @@ export default async function getObject( if (status === 304 || status === 412) { return { body: '', status, contentLength: 0 }; } - return { body: '', status, contentLength: 0 }; + const error = status >= 500 ? e.message : undefined; + return { + body: '', status, contentLength: 0, ...(error ? { error } : {}), + }; } } diff --git a/src/storage/object/move.js b/src/storage/object/move.js index d8324132..e0814425 100644 --- a/src/storage/object/move.js +++ b/src/storage/object/move.js @@ -80,7 +80,7 @@ export default async function moveObject(env, daCtx, details) { // eslint-disable-next-line no-console console.error('Move partial failure', r.reason); }); - return { body: JSON.stringify({ error: 'partial_failure', failed: failed.length }), status: 500 }; + return { body: JSON.stringify({ error: 'partial_failure', failed: failed.length }), status: 500, error: failed[0]?.reason?.message }; } results.push(...settled.map((r) => r.value)); @@ -88,7 +88,7 @@ export default async function moveObject(env, daCtx, details) { } catch (e) { // eslint-disable-next-line no-console console.error('Move failed', e); - return { body: JSON.stringify({ error: 'move_failed' }), status: 500 }; + return { body: JSON.stringify({ error: 'move_failed' }), status: 500, error: e.message }; } } while (ContinuationToken); diff --git a/src/storage/version/audit.js b/src/storage/version/audit.js index 5b9da051..7d58f421 100644 --- a/src/storage/version/audit.js +++ b/src/storage/version/audit.js @@ -278,6 +278,6 @@ export async function writeAuditEntry(env, ctx, repo, fileId, entry, attempt = 0 } catch (e) { // eslint-disable-next-line no-console console.error('writeAuditEntry failed', e); - return { status: 500 }; + return { status: 500, error: e.message }; } } diff --git a/src/storage/version/put.js b/src/storage/version/put.js index ab556dcb..4d824a1d 100644 --- a/src/storage/version/put.js +++ b/src/storage/version/put.js @@ -69,7 +69,7 @@ export async function putVersion(config, { // Cancel the body stream if it wasn't consumed (e.g. R2 rejected via 100-continue on 412). // Without this, Cloudflare Workers logs "non-retryable streaming request" warnings. if (Body?.cancel) Body.cancel(); - return { status }; + return { status, ...(status >= 500 ? { error: e.message } : {}) }; } } @@ -189,7 +189,7 @@ export async function putObjectWithVersion( // eslint-disable-next-line no-console if (status >= 500) console.error('Failed to put object (in object with version)', e); - return { status, metadata: { id: ID } }; + return { status, metadata: { id: ID }, ...(status >= 500 ? { error: e.message } : {}) }; } } @@ -319,7 +319,7 @@ export async function putObjectWithVersion( // eslint-disable-next-line no-console if (status >= 500) console.error('Failed to version (in object with version)', e); - return { status, metadata: { id: ID } }; + return { status, metadata: { id: ID }, ...(status >= 500 ? { error: e.message } : {}) }; } } @@ -335,7 +335,7 @@ export async function postObjectVersionWithLabel(label, env, daCtx) { }, true); if (resp.status !== 200) return { status: resp.status }; - if (!resp.versionCreated) return { status: 500 }; + if (!resp.versionCreated) return { status: 500, error: 'Version was not created' }; return { status: 201 }; } diff --git a/src/utils/daResp.js b/src/utils/daResp.js index c1cdc9de..ef67fd90 100644 --- a/src/utils/daResp.js +++ b/src/utils/daResp.js @@ -30,12 +30,13 @@ export default function daResp({ metadata, etag, continuationToken, + error, }, ctx = null) { const headers = new Headers(); headers.append('Access-Control-Allow-Origin', '*'); headers.append('Access-Control-Allow-Methods', 'HEAD, GET, PUT, POST, DELETE'); headers.append('Access-Control-Allow-Headers', '*'); - headers.append('Access-Control-Expose-Headers', 'X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag'); + headers.append('Access-Control-Expose-Headers', 'X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag, x-error'); headers.append('Content-Type', normalizeCharset(contentType)); if (contentLength) { headers.append('Content-Length', contentLength); @@ -55,6 +56,10 @@ export default function daResp({ headers.append('da-continuation-token', continuationToken); } + if (status >= 500 && error) { + headers.append('x-error', error); + } + if (ctx?.aclCtx && status < 500) { headers.append('X-da-actions', `/${ctx.key}=${[...ctx.aclCtx.actionSet]}`); diff --git a/test/storage/object/move.test.js b/test/storage/object/move.test.js index 6ab758c4..20793876 100644 --- a/test/storage/object/move.test.js +++ b/test/storage/object/move.test.js @@ -118,6 +118,7 @@ describe('Move', () => { const resp = await moveObject({}, ctx, { source: 'somewhere', destination: 'somedest' }); assert.strictEqual(resp.status, 500); + assert.strictEqual(resp.error, 'R2 throttled'); const body = JSON.parse(resp.body); assert.strictEqual(body.error, 'move_failed'); }); @@ -161,6 +162,7 @@ describe('Move', () => { // a.html (from initialKeys) throws, b.html succeeds — one failure, one success assert.strictEqual(resp.status, 500); + assert.strictEqual(resp.error, 'R2 throttled'); const body = JSON.parse(resp.body); assert.strictEqual(body.error, 'partial_failure'); assert.strictEqual(body.failed, 1); diff --git a/test/storage/version/audit.test.js b/test/storage/version/audit.test.js index 523b339e..f7f0d851 100644 --- a/test/storage/version/audit.test.js +++ b/test/storage/version/audit.test.js @@ -511,6 +511,7 @@ describe('Version Audit', () => { }); assert.strictEqual(result.status, 500); + assert.strictEqual(result.error, 'server error'); }); it('appends three entries when edit then version then edit (version breaks time window)', async () => { @@ -851,6 +852,7 @@ describe('Version Audit', () => { }); assert.strictEqual(result.status, 500, 'persistent 412 must surface as 500 after one retry'); + assert.strictEqual(result.error, 'precondition failed'); }); }); }); diff --git a/test/storage/version/put.test.js b/test/storage/version/put.test.js index 2e54efc9..8a5d96e7 100644 --- a/test/storage/version/put.test.js +++ b/test/storage/version/put.test.js @@ -228,6 +228,7 @@ describe('Version Put', () => { const mockCtx = { users: [{ email: 'blah@acme.com' }] }; const resp = await putObjectWithVersion(mockEnv, mockCtx, mockUpdate, true); assert.equal(500, resp.status); + assert.strictEqual(resp.error, 'testing 123'); }); it('Put Object With Version store content', async () => { @@ -788,9 +789,11 @@ describe('Version Put', () => { s3Client = s3client2; const resp2 = await putVersion({}, { Body: 'hello' }); assert.equal(500, resp2.status); + assert.strictEqual(resp2.error, 'Test error2'); s3Client = s3client3; const resp3 = await putVersion({}, { Body: 'hello' }); assert.equal(500, resp3.status); + assert.strictEqual(resp3.error, 'Test error3'); }); it('Test putVersion preserves ContentType', async () => { @@ -2298,6 +2301,7 @@ describe('Version Put', () => { const resp = await postObjectVersion(req, env, ctx); assert.equal(500, resp.status); + assert.strictEqual(resp.error, 'Version was not created'); }); it('postObjectVersion returns 201 when version is successfully created', async () => { @@ -2965,6 +2969,7 @@ describe('Version Put', () => { // No label means shouldCreateVersionObject is false → versionCreated stays false → 500 assert.strictEqual(resp.status, 500); + assert.strictEqual(resp.error, 'Version was not created'); }); }); @@ -3066,6 +3071,7 @@ describe('Version Put', () => { const resp = await postObjectVersionWithLabel('My Label', {}, daCtx); assert.strictEqual(resp.status, 500, 'must return 500 when version was not created'); + assert.strictEqual(resp.error, 'Version was not created'); }); }); }); diff --git a/test/utils/daResp.test.js b/test/utils/daResp.test.js index a2514e43..bacf1b29 100644 --- a/test/utils/daResp.test.js +++ b/test/utils/daResp.test.js @@ -28,7 +28,7 @@ describe('DA Resp', () => { assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Origin')); assert.strictEqual('HEAD, GET, PUT, POST, DELETE', resp.headers.get('Access-Control-Allow-Methods')); assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Headers')); - assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); + assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag, x-error', resp.headers.get('Access-Control-Expose-Headers')); assert.strictEqual('text/plain; charset=utf-8', resp.headers.get('Content-Type')); assert.strictEqual('777', resp.headers.get('Content-Length')); assert.strictEqual('/foo/bar.html=read,write', resp.headers.get('X-da-actions')); @@ -49,7 +49,7 @@ describe('DA Resp', () => { assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Origin')); assert.strictEqual('HEAD, GET, PUT, POST, DELETE', resp.headers.get('Access-Control-Allow-Methods')); assert.strictEqual('*', resp.headers.get('Access-Control-Allow-Headers')); - assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); + assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag, x-error', resp.headers.get('Access-Control-Expose-Headers')); assert.strictEqual('application/json', resp.headers.get('Content-Type')); assert(!resp.headers.get('Content-Length')); assert.strictEqual('/foo/blah.html=read', resp.headers.get('X-da-actions')); @@ -69,6 +69,13 @@ describe('DA Resp', () => { assert(resp.headers.get('X-da-child-actions') === null); assert(resp.headers.get('X-da-acltrace') === null); assert(resp.headers.get('X-da-id') === null); + assert(resp.headers.get('x-error') === null); + }); + + it('test 500 with error message', () => { + const resp = daResp({ status: 500, error: 'Something went wrong' }); + assert.strictEqual(500, resp.status); + assert.strictEqual('Something went wrong', resp.headers.get('x-error')); }); it('normalizes text/html to include charset=utf-8', () => { @@ -136,7 +143,7 @@ describe('DA Resp', () => { const ctx = { key: 'foo/bar.html', aclCtx }; const resp = daResp({ status: 200, body: 'foobar' }, ctx); assert.strictEqual(200, resp.status); - assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag', resp.headers.get('Access-Control-Expose-Headers')); + assert.strictEqual('X-da-actions, X-da-child-actions, X-da-acltrace, X-da-id, da-continuation-token, ETag, x-error', resp.headers.get('Access-Control-Expose-Headers')); assert.strictEqual('/haha/hoho/**=read,write', resp.headers.get('X-da-child-actions')); }); });