From b97e34c1ebef89746f47bc810d48ae388a7bf8c1 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Sun, 27 Mar 2022 00:30:56 +0000 Subject: [PATCH] Flaska: HEAD request support across the board. Flaska: Set status to 204 if status is 200 and body is null Flaska: Fix implementation of how content-type is set FileResponse: Don't open file if running HEAD request --- flaska.mjs | 53 ++++- package.json | 2 +- test/fileResponse.test.mjs | 247 ++++++++++++++------- test/flaska.out.test.mjs | 437 ++++++++++++++++++++++++++----------- test/http.test.mjs | 114 ++++++++++ 5 files changed, 637 insertions(+), 216 deletions(-) diff --git a/flaska.mjs b/flaska.mjs index 33958e7..2db8339 100644 --- a/flaska.mjs +++ b/flaska.mjs @@ -376,10 +376,13 @@ export class FileResponse { ctx.type = found[found.length - 1] } - let stream = useFs.createReadStream(this.filepath, readOptions) ctx.headers['Last-Modified'] = lastModified ctx.headers['Content-Length'] = size - return stream + if (ctx.method !== 'HEAD') { + let stream = useFs.createReadStream(this.filepath, readOptions) + return stream + } + return null } } @@ -912,7 +915,8 @@ ctx.state.nonce = nonce; requestEnd(orgErr, ctx) { let err = orgErr - if (ctx.body && ctx.body.handleRequest) { + let handleUsed = Boolean(ctx.body && ctx.body.handleRequest) + if (handleUsed) { try { ctx.body = ctx.body.handleRequest(ctx) } catch (newErr) { @@ -931,13 +935,18 @@ ctx.state.nonce = nonce; return } + if (ctx.body === null && !handleUsed && ctx.status === 200) { + ctx.status = 204 + } + if (statuses.empty[ctx.status]) { ctx.res.writeHead(ctx.status, ctx.headers) return ctx.res.end() } let body = ctx.body - let length = 0 + + // Special handling for files if (body && typeof(body.pipe) === 'function') { // Be smart when handling file handles, auto detect mime-type // based off of the extension of the file. @@ -951,22 +960,42 @@ ctx.state.nonce = nonce; ctx.headers['Content-Type'] = ctx.type || 'application/octet-stream' ctx.res.writeHead(ctx.status, ctx.headers) - return this.stream.pipeline(body, ctx.res, function() { }) + + if (ctx.method !== 'HEAD') { + return this.stream.pipeline(body, ctx.res, function() { }) + } else { + try { + body.destroy() + } catch { } + return ctx.res.end() + } } - if (typeof(body) === 'object') { + + let length = 0 + + if (typeof(body) === 'object' && body) { body = JSON.stringify(body) length = Buffer.byteLength(body) - ctx.headers['Content-Type'] = 'application/json; charset=utf-8' - } else { + ctx.type = 'application/json; charset=utf-8' + } else if (body) { body = body.toString() length = Buffer.byteLength(body) - ctx.headers['Content-Type'] = ctx.type || 'text/plain; charset=utf-8' + ctx.type = ctx.type || 'text/plain; charset=utf-8' + } + + if (ctx.type) { + ctx.headers['Content-Type'] = ctx.type + } + if (!ctx.headers['Content-Length']) { + ctx.headers['Content-Length'] = length } - - ctx.headers['Content-Length'] = length ctx.res.writeHead(ctx.status, ctx.headers) - ctx.res.end(body) + if (body && ctx.method !== 'HEAD') { + ctx.res.end(body) + } else { + ctx.res.end() + } } requestEnded(ctx) { diff --git a/package.json b/package.json index 6b75325..7513e93 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "flaska", - "version": "1.1.1", + "version": "1.2.0", "description": "Flaska is a micro web-framework for node. It is designed to be fast, simple and lightweight, and is distributed as a single file module with no dependencies.", "main": "flaska.mjs", "scripts": { diff --git a/test/fileResponse.test.mjs b/test/fileResponse.test.mjs index eee070c..3940d26 100644 --- a/test/fileResponse.test.mjs +++ b/test/fileResponse.test.mjs @@ -13,84 +13,86 @@ t.test('should add path and stat', function() { }) t.describe('CreateReader()', function() { - const assertPath = '/some/path/here.png' - const assertBody = { a: 1 } - const assertSize = 12498 - const assertmTime = new Date('2021-04-02T11:22:33.777') - const roundedModified = new Date('2021-04-02T11:22:33') - const assertIno = 3569723027 - const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"` - const stat = { - size: assertSize, - mtime: assertmTime, - ino: assertIno, - } + t.describe('return fileReader', function() { + const assertPath = '/some/path/here.png' + const assertBody = { a: 1 } + const assertSize = 12498 + const assertmTime = new Date('2021-04-02T11:22:33.777') + const roundedModified = new Date('2021-04-02T11:22:33') + const assertIno = 3569723027 + const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"` + const stat = { + size: assertSize, + mtime: assertmTime, + ino: assertIno, + } + + let tests = [ + [ + 'if no range', + function() {}, + ], + [ + 'if range start is higher than end', + function(headers) { headers['range'] = 'bytes=2000-1000' }, + ], + [ + 'if pre-condition if-match passes', + function(headers) { headers['if-match'] = assertEtag }, + ], + [ + 'if pre-condition if-unmodified-since passes', + function(headers) { headers['if-unmodified-since'] = roundedModified.toUTCString(); }, + ], + [ + 'if both pre-condition pass', + function(headers) { headers['if-unmodified-since'] = new Date(roundedModified.getTime() + 1000).toUTCString(); headers['if-match'] = assertEtag }, + ], + [ + 'if range is specified but if-range etag does not match', + function(headers) { + headers['if-range'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() + 1}"` + headers['range'] = 'bytes=1000-2000' + } + ], + [ + 'if range is specified but if-range modified by is older', + function(headers) { + headers['if-range'] = `${new Date(roundedModified.getTime() - 1000).toUTCString()}` + headers['range'] = 'bytes=1000-2000' + } + ], + [ + 'if range is specified but if-range is garbage', + function(headers) { + headers['if-range'] = `asdf` + headers['range'] = 'bytes=1000-2000' + } + ], + ] - let tests = [ - [ - 'if no range', - function() {}, - ], - [ - 'if range start is higher than end', - function(headers) { headers['range'] = 'bytes=2000-1000' }, - ], - [ - 'if pre-condition if-match passes', - function(headers) { headers['if-match'] = assertEtag }, - ], - [ - 'if pre-condition if-unmodified-since passes', - function(headers) { headers['if-unmodified-since'] = roundedModified.toUTCString(); }, - ], - [ - 'if both pre-condition pass', - function(headers) { headers['if-unmodified-since'] = new Date(roundedModified.getTime() + 1000).toUTCString(); headers['if-match'] = assertEtag }, - ], - [ - 'if range is specified but if-range etag does not match', - function(headers) { - headers['if-range'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() + 1}"` - headers['range'] = 'bytes=1000-2000' - } - ], - [ - 'if range is specified but if-range modified by is older', - function(headers) { - headers['if-range'] = `${new Date(roundedModified.getTime() - 1000).toUTCString()}` - headers['range'] = 'bytes=1000-2000' - } - ], - [ - 'if range is specified but if-range is garbage', - function(headers) { - headers['if-range'] = `asdf` - headers['range'] = 'bytes=1000-2000' - } - ], - ] - - tests.forEach(function(test){ - t.test('return fileReader ' + test[0], function() { - - const stubCreateReadStream = stub().returns(assertBody) - let ctx = createCtx() - test[1](ctx.req.headers) - ctx.body = new FileResponse(assertPath, stat) - let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream }) - - assert.strictEqual(checkBody, assertBody) - assert.strictEqual(stubCreateReadStream.firstCall[0], assertPath) - assert.deepStrictEqual(stubCreateReadStream.firstCall[1], {}) - assert.strictEqual(ctx.headers['Etag'], assertEtag) - assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString()) - assert.strictEqual(ctx.headers['Content-Length'], assertSize) - assert.strictEqual(ctx.type, 'image/png') - assert.notOk(ctx.headers['Content-Range']) - assert.strictEqual(ctx.status, 200) + tests.forEach(function(test){ + t.test(test[0], function() { + + const stubCreateReadStream = stub().returns(assertBody) + let ctx = createCtx() + test[1](ctx.req.headers) + ctx.body = new FileResponse(assertPath, stat) + let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream }) + + assert.strictEqual(checkBody, assertBody) + assert.strictEqual(stubCreateReadStream.firstCall[0], assertPath) + assert.deepStrictEqual(stubCreateReadStream.firstCall[1], {}) + assert.strictEqual(ctx.headers['Etag'], assertEtag) + assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString()) + assert.strictEqual(ctx.headers['Content-Length'], assertSize) + assert.strictEqual(ctx.type, 'image/png') + assert.notOk(ctx.headers['Content-Range']) + assert.strictEqual(ctx.status, 200) + }) }) }) - + t.test('return fileReader with proper length with range', function() { const assertPath = '/some/path/here.jpg' const assertBody = { a: 1 } @@ -120,6 +122,101 @@ t.describe('CreateReader()', function() { assert.strictEqual(ctx.status, 206) }) + t.test('should not call createReadStream if HEAD request', function() { + const assertPath = '/some/path/here.png' + const assertNotBody = { a: 1 } + const assertSize = 12498 + const assertmTime = new Date('2021-04-02T11:22:33.777') + const roundedModified = new Date('2021-04-02T11:22:33') + const assertIno = 3569723027 + const assertEtag = `"${assertIno}-${assertSize}-${assertmTime.getTime()}"` + const stat = { + size: assertSize, + mtime: assertmTime, + ino: assertIno, + } + + let tests = [ + [ + 'if no range', + function() {}, + ], + [ + 'if range start is higher than end', + function(headers) { headers['range'] = 'bytes=2000-1000' }, + ], + [ + 'if pre-condition if-match passes', + function(headers) { headers['if-match'] = assertEtag }, + ], + [ + 'if pre-condition if-unmodified-since passes', + function(headers) { headers['if-unmodified-since'] = roundedModified.toUTCString(); }, + ], + [ + 'if both pre-condition pass', + function(headers) { headers['if-unmodified-since'] = new Date(roundedModified.getTime() + 1000).toUTCString(); headers['if-match'] = assertEtag }, + ], + [ + 'if range is specified but if-range etag does not match', + function(headers) { + headers['if-range'] = `"${assertIno}-${assertSize}-${assertmTime.getTime() + 1}"` + headers['range'] = 'bytes=1000-2000' + } + ], + [ + 'if range is specified but if-range modified by is older', + function(headers) { + headers['if-range'] = `${new Date(roundedModified.getTime() - 1000).toUTCString()}` + headers['range'] = 'bytes=1000-2000' + } + ], + [ + 'if range is specified but if-range is garbage', + function(headers) { + headers['if-range'] = `asdf` + headers['range'] = 'bytes=1000-2000' + } + ], + ] + + tests.forEach(function(test){ + const stubCreateReadStream = stub().returns(assertNotBody) + let ctx = createCtx({ + method: 'HEAD', + }) + test[1](ctx.req.headers) + ctx.body = new FileResponse(assertPath, stat) + let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream }) + + assert.strictEqual(checkBody, null) + assert.notOk(stubCreateReadStream.called) + assert.strictEqual(ctx.headers['Etag'], assertEtag) + assert.strictEqual(ctx.headers['Last-Modified'], assertmTime.toUTCString()) + assert.strictEqual(ctx.headers['Content-Length'], assertSize) + assert.strictEqual(ctx.type, 'image/png') + assert.notOk(ctx.headers['Content-Range']) + assert.strictEqual(ctx.status, 200) + }) + + const testStart = [0, 1000, 1999] + testStart.forEach(function(start) { + const stubCreateReadStream = stub() + let ctx = createCtx({ + method: 'HEAD', + }) + ctx.req.headers['range'] = 'bytes=' + start + '-' + ctx.body = new FileResponse('file.png', stat) + let checkBody = ctx.body.handleRequest(ctx, { createReadStream: stubCreateReadStream }) + + assert.strictEqual(checkBody, null) + assert.notOk(stubCreateReadStream.called) + assert.strictEqual(ctx.headers['Content-Length'], assertSize - start) + assert.strictEqual(ctx.headers['Content-Range'], `${start}-${assertSize - 1}/${assertSize}`) + assert.strictEqual(ctx.status, 206) + }) + }) + t.test('return fileReader with proper length with range and if-range passes', function() { const assertPath = '/some/path/here.jpg' const assertBody = { a: 1 } diff --git a/test/flaska.out.test.mjs b/test/flaska.out.test.mjs index 9f74bec..e8609bf 100644 --- a/test/flaska.out.test.mjs +++ b/test/flaska.out.test.mjs @@ -61,110 +61,320 @@ t.describe('#requestEnd()', function() { flaska.requestEnd(assertErrorNotSeen, ctx) }) - t.test('call res and end correctly when dealing with objects', function(cb) { - const assertStatus = 202 - // Calculated manually just in case - const assertBodyLength = 7 - const assertBody = { a: 1 } - let onFinish = function(body) { - try { - assert.strictEqual(ctx.status, assertStatus) - assert.strictEqual(ctx.body, assertBody) - assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8') - assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength) - assert.ok(body) - assert.strictEqual(body, '{"a":1}') - assert.ok(ctx.res.writeHead.called) - assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) - assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) - cb() - } catch (err) { cb(err) } - } - const ctx = createCtx({ - status: assertStatus, - }, onFinish) - ctx.body = assertBody - - let flaska = new Flaska({}, fakerHttp, fakeStream) - flaska.requestEnd(null, ctx) - }) + let testMethods = ['GET', 'HEAD'] - t.test('call res and end correctly when dealing with strings', function(cb) { - const assertStatus = 206 - // Calculated manually just in case - const assertBodyLength = 4 - const assertBody = 'eða' - let onFinish = function(body) { - try { - assert.strictEqual(ctx.status, assertStatus) - assert.strictEqual(ctx.body, assertBody) - assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8') - assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength) - assert.ok(body) - assert.strictEqual(body, assertBody) - assert.ok(ctx.res.writeHead.called) - assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) - assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) - cb() - } catch (err) { cb(err) } - } - const ctx = createCtx({ - status: assertStatus, - }, onFinish) - ctx.body = assertBody + testMethods.forEach(function(method) { + t.describe(method, function() { + t.test('call res and end correctly when dealing with objects', function(cb) { + const assertStatus = 202 + // Calculated manually just in case + const assertBodyLength = 7 + const assertBody = { a: 1 } + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.body, assertBody) + assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8') + assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength) - let flaska = new Flaska({}, fakerHttp, fakeStream) - flaska.requestEnd(null, ctx) - }) - - t.test('call res and end correctly when dealing with numbers', function(cb) { - const assertStatus = 211 - // Calculated manually just in case - const assertBodyLength = 7 - const assertBody = 4214124 - let onFinish = function(body) { - try { - assert.strictEqual(ctx.status, assertStatus) - assert.strictEqual(ctx.body, assertBody) - assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8') - assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength) - assert.ok(body) - assert.strictEqual(body, assertBody.toString()) - assert.ok(ctx.res.writeHead.called) - assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) - assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) - cb() - } catch (err) { cb(err) } - } - const ctx = createCtx({ - status: assertStatus, - }, onFinish) - ctx.body = assertBody + if (method === 'GET') { + assert.ok(body) + assert.strictEqual(body, '{"a":1}') + } else { + assert.notOk(body) + } - let flaska = new Flaska({}, fakerHttp, fakeStream) - flaska.requestEnd(null, ctx) - }) + assert.ok(ctx.res.writeHead.called) + assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) + assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + method: method, + status: assertStatus, + }, onFinish) + ctx.body = assertBody + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) - t.test('call handleRequest if body contains it', function() { - let body = new FileResponse() - let assertBody = { a : 1 } - const ctx = createCtx({ - status: 204, + t.test('call res and end correctly when dealing with null and no status override', function(cb) { + // Calculated manually just in case + const assertBodyLength = 0 + const assertBody = null + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, 204) + assert.strictEqual(ctx.body, assertBody) + assert.notOk(ctx.headers['Content-Type']) + assert.notOk(ctx.headers['Content-Length']) + + assert.strictEqual(body, undefined) + + assert.ok(ctx.res.writeHead.called) + assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) + assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + method: method, + status: 200, + }, onFinish) + ctx.body = assertBody + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + + t.test('call res and end correctly when dealing with null and status override', function(cb) { + const assertStatus = 202 + // Calculated manually just in case + const assertBody = null + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.body, assertBody) + assert.notOk(ctx.headers['Content-Type']) + assert.strictEqual(ctx.headers['Content-Length'], 0) + + assert.strictEqual(body, undefined) + + assert.ok(ctx.res.writeHead.called) + assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) + assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + method: method, + status: assertStatus, + }, onFinish) + ctx.body = assertBody + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + + t.test('call res and end correctly when dealing with strings', function(cb) { + const assertStatus = 206 + // Calculated manually just in case + const assertBodyLength = 4 + const assertBody = 'eða' + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.body, assertBody) + assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8') + assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength) + + if (method === 'GET') { + assert.ok(body) + assert.strictEqual(body, assertBody) + } else { + assert.notOk(body) + } + + assert.ok(ctx.res.writeHead.called) + assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) + assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + method: method, + status: assertStatus, + }, onFinish) + ctx.body = assertBody + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + + t.test('call res and end correctly when dealing with numbers', function(cb) { + const assertStatus = 211 + // Calculated manually just in case + const assertBodyLength = 7 + const assertBody = 4214124 + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.body, assertBody) + assert.strictEqual(ctx.headers['Content-Type'], 'text/plain; charset=utf-8') + assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength) + + if (method === 'GET') { + assert.ok(body) + assert.strictEqual(body, assertBody.toString()) + } else { + assert.notOk(body) + } + assert.ok(ctx.res.writeHead.called) + assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) + assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + method: method, + status: assertStatus, + }, onFinish) + ctx.body = assertBody + + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + + if (method === 'GET') { + t.test('call handleRequest if body contains it', function(cb) { + const assertContentType = 'test/test' + const assertContentLength = 1241241 + const assertHandle = stub().returnWith(function(checkCtx, secondary) { + assert.notOk(secondary) + assert.strictEqual(checkCtx, ctx) + assert.notOk(ctx.res.writeHead.called) + ctx.type = assertContentType + ctx.headers['Content-Length'] = assertContentLength + return assertBody + }) + + let body = new FileResponse() + let assertBody = { pipe: function() {} } + let onFinish = function(source, target, callback) { + try { + assert.ok(assertHandle.called) + assert.ok(ctx.res.writeHead) + assert.strictEqual(ctx.body, assertBody) + assert.strictEqual(source, assertBody) + assert.strictEqual(target, ctx.res) + assert.strictEqual(typeof(callback), 'function') + assert.ok(ctx.res.writeHead.called) + assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) + assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) + assert.strictEqual(ctx.headers['Content-Type'], assertContentType) + assert.strictEqual(ctx.headers['Content-Length'], assertContentLength) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + }) + ctx.body = body + fakeStream.pipeline = onFinish + body.handleRequest = assertHandle + + let flaska = new Flaska({}, fakerHttp, fakeStream) + + try { + flaska.requestEnd(null, ctx) + } catch (err) { + cb(err) + } + }) + + t.test('call pipeline correctly when dealing with pipe', function(cb) { + const assertStatus = 211 + const assertType = 'herp/derp' + const assertBody = { pipe: function() {} } + + let onFinish = function(source, target, callback) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.headers['Content-Type'], assertType) + assert.strictEqual(source, assertBody) + assert.strictEqual(target, ctx.res) + assert.strictEqual(typeof(callback), 'function') + assert.ok(ctx.res.writeHead.called) + assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) + assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + status: assertStatus, + }) + fakeStream.pipeline = onFinish + + ctx.body = assertBody + ctx.type = assertType + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + } else { + t.test('call handleRequest if body contains it', function(cb) { + const assertContentType = 'test/test' + const assertContentLength = 1241241 + const assertHandle = stub().returnWith(function(checkCtx, secondary) { + assert.notOk(secondary) + assert.strictEqual(checkCtx, ctx) + assert.notOk(ctx.res.writeHead.called) + ctx.type = assertContentType + ctx.headers['Content-Length'] = assertContentLength + return null + }) + + let body = new FileResponse() + let onFinish = function(body) { + try { + assert.ok(assertHandle.called) + assert.ok(ctx.res.writeHead) + assert.strictEqual(ctx.body, null) + assert.notOk(body) + assert.ok(ctx.res.writeHead.called) + assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) + assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) + assert.strictEqual(ctx.headers['Content-Type'], assertContentType) + assert.strictEqual(ctx.headers['Content-Length'], assertContentLength) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + method: method, + }, onFinish) + ctx.body = body + body.handleRequest = assertHandle + + let flaska = new Flaska({}, fakerHttp, fakeStream) + + try { + flaska.requestEnd(null, ctx) + } catch (err) { + cb(err) + } + }) + + t.test('call pipeline correctly when dealing with pipe', function(cb) { + const assertStatus = 211 + const assertType = 'herp/derp' + const assertBody = { pipe: function() {}, destroy: stub() } + + let onFinish = function(body) { + try { + assert.strictEqual(ctx.status, assertStatus) + assert.strictEqual(ctx.headers['Content-Type'], assertType) + assert.notOk(ctx.headers['Content-Length']) + assert.notOk(body) + + assert.ok(assertBody.destroy.called) + assert.ok(ctx.res.writeHead.called) + assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) + assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) + cb() + } catch (err) { cb(err) } + } + const ctx = createCtx({ + method: method, + status: assertStatus, + }, onFinish) + + ctx.body = assertBody + ctx.type = assertType + let flaska = new Flaska({}, fakerHttp, fakeStream) + flaska.requestEnd(null, ctx) + }) + } }) - ctx.body = body - const assertHandle = stub().returnWith(function(checkCtx, secondary) { - assert.notOk(secondary) - assert.strictEqual(checkCtx, ctx) - assert.notOk(ctx.res.writeHead.called) - return assertBody - }) - body.handleRequest = assertHandle - - let flaska = new Flaska({}, fakerHttp, fakeStream) - flaska.requestEnd(null, ctx) - assert.ok(assertHandle.called) - assert.ok(ctx.res.writeHead) - assert.strictEqual(ctx.body, assertBody) }) t.test('call _onerror with error if handleRequest throws error', function() { @@ -216,35 +426,6 @@ t.describe('#requestEnd()', function() { flaska.requestEnd(null, ctx) }) - t.test('call pipeline correctly when dealing with pipe', function(cb) { - const assertStatus = 211 - const assertType = 'herp/derp' - const assertBody = { pipe: function() {} } - - let onFinish = function(source, target, callback) { - try { - assert.strictEqual(ctx.status, assertStatus) - assert.strictEqual(ctx.headers['Content-Type'], assertType) - assert.strictEqual(source, assertBody) - assert.strictEqual(target, ctx.res) - assert.strictEqual(typeof(callback), 'function') - assert.ok(ctx.res.writeHead.called) - assert.strictEqual(ctx.res.writeHead.firstCall[0], ctx.status) - assert.strictEqual(ctx.res.writeHead.firstCall[1], ctx.headers) - cb() - } catch (err) { cb(err) } - } - const ctx = createCtx({ - status: assertStatus, - }) - fakeStream.pipeline = onFinish - - ctx.body = assertBody - ctx.type = assertType - let flaska = new Flaska({}, fakerHttp, fakeStream) - flaska.requestEnd(null, ctx) - }) - t.test('call pipe should have default type', function(cb) { let onFinish = function(source, target) { try { diff --git a/test/http.test.mjs b/test/http.test.mjs index c4568d1..3c9592e 100644 --- a/test/http.test.mjs +++ b/test/http.test.mjs @@ -285,6 +285,120 @@ t.describe('/file/upload', function() { }) }) +t.describe('HEAD', function() { + const agent = new http.Agent({ + keepAlive: true, + maxSockets: 1, + keepAliveMsecs: 3000, + }) + + t.describe('/file', function() { + t.test('server return HEAD for pipes', async function() { + log.error.reset() + + let res = await client.customRequest('HEAD', '/file', null, { getRaw: true, agent: agent }) + + while (!file.closed) { + await setTimeout(10) + } + + assert.ok(file.closed) + + let statSource = await fs.stat('./test/test.png') + + assert.strictEqual(res.data, '') + assert.strictEqual(res.headers['content-type'], 'image/png') + assert.notOk(res.headers['content-length']) + }) + t.test('server should autoclose body file handles on errors', async function() { + reset() + + file = null + + let req = await client.customRequest('HEAD', '/file/leak', null, { returnRequest: true }) + + req.end() + + while (!file) { + await setTimeout(10) + } + + assert.ok(file) + assert.notOk(file.closed) + + req.destroy() + + while (!file.closed) { + await setTimeout(10) + } + + assert.strictEqual(log.error.callCount, 0) + assert.strictEqual(log.info.callCount, 1) + assert.strictEqual(log.info.firstCall[0].message, 'aborted') + + assert.ok(file.closed) + }) + }) + + t.describe('/filehandle', function() { + t.test('server should send correctly', async function() { + log.error.reset() + + let res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent }) + assert.strictEqual(res.status, 200) + assert.strictEqual(res.headers['content-length'], '11') + assert.strictEqual(res.headers['content-type'], 'text/plain') + assert.match(res.headers['etag'], /\"\d+-11-\d+"/) + assert.ok(res.headers['last-modified']) + let d = new Date(Date.parse(res.headers['last-modified'])) + let etag = res.headers['etag'] + assert.ok(d.getTime()) + assert.strictEqual(res.data, '') + + res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, + headers: { + 'If-Modified-Since': d.toUTCString() + }, + }) + assert.strictEqual(res.status, 304) + assert.strictEqual(res.data, '') + assert.strictEqual(res.headers['etag'], etag) + + res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, + headers: { + 'If-None-Match': etag + }, + }) + assert.strictEqual(res.status, 304) + assert.strictEqual(res.data, '') + assert.strictEqual(res.headers['etag'], etag) + + res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, + headers: { + 'Range': 'bytes=2-5' + }, + }) + assert.strictEqual(res.status, 206) + assert.strictEqual(res.data, '') + assert.strictEqual(res.headers['content-length'], '4') + + + res = await client.customRequest('HEAD', '/filehandle', null, { getRaw: true, agent: agent, + headers: { + 'Range': 'bytes=0-0' + }, + }) + assert.strictEqual(res.status, 206) + assert.strictEqual(res.data, '') + assert.strictEqual(res.headers['content-length'], '1') + }) + + t.after(function() { + agent.destroy() + }) + }) +}) + t.after(function() { return Promise.all([ fs.rm('./test_tmp.png', { force: true }),