Flaska: HEAD request support across the board.
All checks were successful
continuous-integration/appveyor/branch AppVeyor build succeeded

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
This commit is contained in:
Jonatan Nilsson 2022-03-27 00:30:56 +00:00
parent d4bac4940e
commit b97e34c1eb
5 changed files with 637 additions and 216 deletions

View file

@ -376,11 +376,14 @@ 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
if (ctx.method !== 'HEAD') {
let stream = useFs.createReadStream(this.filepath, readOptions)
return stream
}
return null
}
}
export class FlaskaRouter {
@ -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)
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.res.writeHead(ctx.status, ctx.headers)
if (body && ctx.method !== 'HEAD') {
ctx.res.end(body)
} else {
ctx.res.end()
}
}
requestEnded(ctx) {

View file

@ -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": {

View file

@ -13,6 +13,7 @@ t.test('should add path and stat', function() {
})
t.describe('CreateReader()', function() {
t.describe('return fileReader', function() {
const assertPath = '/some/path/here.png'
const assertBody = { a: 1 }
const assertSize = 12498
@ -71,7 +72,7 @@ t.describe('CreateReader()', function() {
]
tests.forEach(function(test){
t.test('return fileReader ' + test[0], function() {
t.test(test[0], function() {
const stubCreateReadStream = stub().returns(assertBody)
let ctx = createCtx()
@ -90,6 +91,7 @@ t.describe('CreateReader()', function() {
assert.strictEqual(ctx.status, 200)
})
})
})
t.test('return fileReader with proper length with range', function() {
const assertPath = '/some/path/here.jpg'
@ -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 }

View file

@ -61,6 +61,10 @@ t.describe('#requestEnd()', function() {
flaska.requestEnd(assertErrorNotSeen, ctx)
})
let testMethods = ['GET', 'HEAD']
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
@ -72,8 +76,14 @@ t.describe('#requestEnd()', function() {
assert.strictEqual(ctx.body, assertBody)
assert.strictEqual(ctx.headers['Content-Type'], 'application/json; charset=utf-8')
assert.strictEqual(ctx.headers['Content-Length'], assertBodyLength)
if (method === 'GET') {
assert.ok(body)
assert.strictEqual(body, '{"a":1}')
} 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)
@ -81,6 +91,65 @@ t.describe('#requestEnd()', function() {
} 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 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
@ -100,8 +169,14 @@ t.describe('#requestEnd()', function() {
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)
@ -109,6 +184,7 @@ t.describe('#requestEnd()', function() {
} catch (err) { cb(err) }
}
const ctx = createCtx({
method: method,
status: assertStatus,
}, onFinish)
ctx.body = assertBody
@ -128,8 +204,13 @@ t.describe('#requestEnd()', function() {
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)
@ -137,6 +218,7 @@ t.describe('#requestEnd()', function() {
} catch (err) { cb(err) }
}
const ctx = createCtx({
method: method,
status: assertStatus,
}, onFinish)
ctx.body = assertBody
@ -145,26 +227,154 @@ t.describe('#requestEnd()', function() {
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,
})
ctx.body = body
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
})
body.handleRequest = assertHandle
let flaska = new Flaska({}, fakerHttp, fakeStream)
flaska.requestEnd(null, ctx)
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)
})
}
})
})
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 {

View file

@ -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 }),