diff --git a/src/handlers/head.js b/src/handlers/head.js index ac93c092..e7e6969c 100644 --- a/src/handlers/head.js +++ b/src/handlers/head.js @@ -33,7 +33,7 @@ export default async function headHandler({ env, daCtx }) { if (path.startsWith('/list')) { const { body, contentType, status } = await getList({ env, daCtx }); return { - contentLength: body.length, + contentLength: body?.length ?? 0, contentType, status, }; @@ -41,7 +41,7 @@ export default async function headHandler({ env, daCtx }) { if (path.startsWith('/versionlist')) { const { body, contentType, status } = await getVersionList({ env, daCtx }); return { - contentLength: body.length, + contentLength: body?.length ?? 0, contentType, status, }; diff --git a/src/index.js b/src/index.js index 30d2eefd..56e14b8a 100644 --- a/src/index.js +++ b/src/index.js @@ -65,24 +65,29 @@ export default { } let respObj; - switch (req.method) { - case 'HEAD': - respObj = await headHandler({ env, daCtx }); - break; - case 'GET': - respObj = await getHandler({ env, daCtx }); - break; - case 'PUT': - respObj = await postHandler({ req, env, daCtx }); - break; - case 'POST': - respObj = await postHandler({ req, env, daCtx }); - break; - case 'DELETE': - respObj = await deleteHandler({ req, env, daCtx }); - break; - default: - respObj = { status: 405 }; + try { + switch (req.method) { + case 'HEAD': + respObj = await headHandler({ env, daCtx }); + break; + case 'GET': + respObj = await getHandler({ env, daCtx }); + break; + case 'PUT': + respObj = await postHandler({ req, env, daCtx }); + break; + case 'POST': + respObj = await postHandler({ req, env, daCtx }); + break; + case 'DELETE': + respObj = await deleteHandler({ req, env, daCtx }); + break; + default: + respObj = { status: 405 }; + } + } catch (e) { + console.error('Error handling request', e); + return daResp({ status: 500, error: e.message }); } if (!respObj) return daResp({ status: 404 }); diff --git a/test/handlers/head.test.js b/test/handlers/head.test.js new file mode 100644 index 00000000..1be1505b --- /dev/null +++ b/test/handlers/head.test.js @@ -0,0 +1,59 @@ +/* + * Copyright 2025 Adobe. All rights reserved. + * This file is licensed to you under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. You may obtain a copy + * of the License at http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under + * the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import assert from 'node:assert'; +import esmock from 'esmock'; + +describe('Head Route', () => { + it('returns 404 with no content length', async () => { + const headHandler = (await import('../../src/handlers/head.js')).default; + const daCtx = { path: '/favicon.ico' }; + const resp = await headHandler({ env: {}, daCtx }); + assert.strictEqual(resp.status, 404); + assert.strictEqual(resp.contentLength, 0); + }); + + it('returns 403 without crashing when HEAD /list has no read permission', async () => { + const getList = async () => ({ status: 403 }); + const headHandler = await esmock('../../src/handlers/head.js', { + '../../src/routes/list.js': { default: getList }, + }); + + const daCtx = { path: '/list/foo/bar' }; + const resp = await headHandler({ env: {}, daCtx }); + assert.strictEqual(resp.status, 403); + assert.strictEqual(resp.contentLength, 0); + }); + + it('returns 403 without crashing when HEAD /versionlist has no read permission', async () => { + const getVersionList = async () => ({ status: 403 }); + const headHandler = await esmock('../../src/handlers/head.js', { + '../../src/routes/version.js': { getVersionList }, + }); + + const daCtx = { path: '/versionlist/foo/bar' }; + const resp = await headHandler({ env: {}, daCtx }); + assert.strictEqual(resp.status, 403); + assert.strictEqual(resp.contentLength, 0); + }); + + it('returns contentLength for a successful HEAD /list', async () => { + const getList = async () => ({ status: 200, body: '[{"foo":"bar"}]', contentType: 'application/json' }); + const headHandler = await esmock('../../src/handlers/head.js', { + '../../src/routes/list.js': { default: getList }, + }); + + const daCtx = { path: '/list/foo/bar' }; + const resp = await headHandler({ env: {}, daCtx }); + assert.strictEqual(resp.status, 200); + assert.strictEqual(resp.contentLength, 15); + }); +}); diff --git a/test/index.test.js b/test/index.test.js index f4f6336c..441f2bb4 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -135,6 +135,45 @@ describe('fetch', () => { assert.strictEqual(resp.status, 500); }); + it('should return 500 with x-error header when a handler throws unexpectedly', async () => { + const hnd = await esmock('../src/index.js', { + '../src/utils/daCtx.js': { + default: async () => ({ + authorized: true, + users: [{ email: 'test@example.com' }], + path: '/source/org/repo/file.html', + }), + }, + '../src/handlers/get.js': { + default: async () => { + throw new Error('Unexpected handler error'); + }, + }, + }); + + const resp = await hnd.fetch({ method: 'GET', url: 'http://www.example.com/source/org/repo/file.html' }, {}); + assert.strictEqual(resp.status, 500); + assert.strictEqual(resp.headers.get('x-error'), 'Unexpected handler error'); + }); + + it('should dispatch HEAD requests to the head handler', async () => { + const hnd = await esmock('../src/index.js', { + '../src/utils/daCtx.js': { + default: async () => ({ + authorized: true, + users: [{ email: 'test@example.com' }], + path: '/source/org/repo/file.html', + }), + }, + '../src/handlers/head.js': { + default: async () => ({ status: 200, contentLength: 0 }), + }, + }); + + const resp = await hnd.fetch({ method: 'HEAD', url: 'http://www.example.com/source/org/repo/file.html' }, {}); + assert.strictEqual(resp.status, 200); + }); + it('should expose continuation token header for list responses', async () => { const hnd = await esmock('../src/index.js', { '../src/utils/daCtx.js': {