Skip to content

Commit d28f7d3

Browse files
authored
feat: extends send result to provide ability of custom handling (#80)
* feat: extends send result to provide ability of custom handling * feat: ensure error exists * fixup
1 parent ab277e4 commit d28f7d3

File tree

7 files changed

+316
-15
lines changed

7 files changed

+316
-15
lines changed

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,75 @@ var server = http.createServer(function onRequest (req, res) {
216216
server.listen(3000)
217217
```
218218

219+
### Custom directory index view
220+
221+
This is an example of serving up a structure of directories with a
222+
custom function to render a listing of a directory.
223+
224+
```js
225+
var http = require('node:http')
226+
var fs = require('node:fs')
227+
var parseUrl = require('parseurl')
228+
var send = require('@fastify/send')
229+
230+
// Transfer arbitrary files from within /www/example.com/public/*
231+
// with a custom handler for directory listing
232+
var server = http.createServer(async function onRequest (req, res) {
233+
const { statusCode, headers, stream, type, metadata } = await send(req, parseUrl(req).pathname, { index: false, root: '/www/public' })
234+
if(type === 'directory') {
235+
// get directory list
236+
const list = await readdir(metadata.path)
237+
// render an index for the directory
238+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=UTF-8' })
239+
res.end(list.join('\n') + '\n')
240+
} else {
241+
res.writeHead(statusCode, headers)
242+
stream.pipe(res)
243+
}
244+
})
245+
246+
server.listen(3000)
247+
```
248+
249+
### Serving from a root directory with custom error-handling
250+
251+
```js
252+
var http = require('node:http')
253+
var parseUrl = require('parseurl')
254+
var send = require('@fastify/send')
255+
256+
var server = http.createServer(async function onRequest (req, res) {
257+
// transfer arbitrary files from within
258+
// /www/example.com/public/*
259+
const { statusCode, headers, stream, type, metadata } = await send(req, parseUrl(req).pathname, { root: '/www/public' })
260+
switch (type) {
261+
case 'directory': {
262+
// your custom directory handling logic:
263+
res.writeHead(301, {
264+
'Location': metadata.requestPath + '/'
265+
})
266+
res.end('Redirecting to ' + metadata.requestPath + '/')
267+
break
268+
}
269+
case 'error': {
270+
// your custom error-handling logic:
271+
res.writeHead(metadata.error.status ?? 500, {})
272+
res.end(metadata.error.message)
273+
break
274+
}
275+
default: {
276+
// your custom headers
277+
// serve all files for download
278+
res.setHeader('Content-Disposition', 'attachment')
279+
res.writeHead(statusCode, headers)
280+
stream.pipe(res)
281+
}
282+
}
283+
})
284+
285+
server.listen(3000)
286+
```
287+
219288
## License
220289
221290
[MIT](LICENSE)

lib/createHttpError.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict'
2+
3+
const createError = require('http-errors')
4+
5+
/**
6+
* Create a HttpError object from simple arguments.
7+
*
8+
* @param {number} status
9+
* @param {Error|object} err
10+
* @private
11+
*/
12+
13+
function createHttpError (status, err) {
14+
if (!err) {
15+
return createError(status)
16+
}
17+
18+
return err instanceof Error
19+
? createError(status, err, { expose: false })
20+
: createError(status, err)
21+
}
22+
23+
module.exports.createHttpError = createHttpError

lib/send.js

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ const { isUtf8MimeType } = require('../lib/isUtf8MimeType')
1818
const { normalizeList } = require('../lib/normalizeList')
1919
const { parseBytesRange } = require('../lib/parseBytesRange')
2020
const { parseTokenList } = require('./parseTokenList')
21+
const { createHttpError } = require('./createHttpError')
2122

2223
/**
2324
* Path function references.
@@ -403,7 +404,10 @@ function sendError (statusCode, err) {
403404
return {
404405
statusCode,
405406
headers,
406-
stream: Readable.from(doc[0])
407+
stream: Readable.from(doc[0]),
408+
// metadata
409+
type: 'error',
410+
metadata: { error: createHttpError(statusCode, err) }
407411
}
408412
}
409413

@@ -427,7 +431,7 @@ function sendStatError (err) {
427431
* @api private
428432
*/
429433

430-
function sendNotModified (headers) {
434+
function sendNotModified (headers, path, stat) {
431435
debug('not modified')
432436

433437
delete headers['Content-Encoding']
@@ -439,7 +443,10 @@ function sendNotModified (headers) {
439443
return {
440444
statusCode: 304,
441445
headers,
442-
stream: Readable.from('')
446+
stream: Readable.from(''),
447+
// metadata
448+
type: 'file',
449+
metadata: { path, stat }
443450
}
444451
}
445452

@@ -498,7 +505,7 @@ function sendFileDirectly (request, path, stat, options) {
498505
}
499506

500507
if (isNotModifiedFailure(request, headers)) {
501-
return sendNotModified(headers)
508+
return sendNotModified(headers, path, stat)
502509
}
503510
}
504511

@@ -556,23 +563,37 @@ function sendFileDirectly (request, path, stat, options) {
556563

557564
// HEAD support
558565
if (request.method === 'HEAD') {
559-
return { statusCode, headers, stream: Readable.from('') }
566+
return {
567+
statusCode,
568+
headers,
569+
stream: Readable.from(''),
570+
// metadata
571+
type: 'file',
572+
metadata: { path, stat }
573+
}
560574
}
561575

562576
const stream = fs.createReadStream(path, {
563577
start: offset,
564578
end: Math.max(offset, offset + len - 1)
565579
})
566580

567-
return { statusCode, headers, stream }
581+
return {
582+
statusCode,
583+
headers,
584+
stream,
585+
// metadata
586+
type: 'file',
587+
metadata: { path, stat }
588+
}
568589
}
569590

570-
function sendRedirect (path) {
571-
if (hasTrailingSlash(path)) {
591+
function sendRedirect (path, options) {
592+
if (hasTrailingSlash(options.path)) {
572593
return sendError(403)
573594
}
574595

575-
const loc = encodeURI(collapseLeadingSlashes(path + '/'))
596+
const loc = encodeURI(collapseLeadingSlashes(options.path + '/'))
576597
const doc = createHtmlDocument('Redirecting', 'Redirecting to <a href="' + escapeHtml(loc) + '">' +
577598
escapeHtml(loc) + '</a>')
578599

@@ -586,7 +607,10 @@ function sendRedirect (path) {
586607
return {
587608
statusCode: 301,
588609
headers,
589-
stream: Readable.from(doc[0])
610+
stream: Readable.from(doc[0]),
611+
// metadata
612+
type: 'directory',
613+
metadata: { requestPath: options.path, path }
590614
}
591615
}
592616

@@ -636,7 +660,7 @@ async function sendFile (request, path, options) {
636660
return sendError(404)
637661
}
638662
if (error) return sendStatError(error)
639-
if (stat.isDirectory()) return sendRedirect(options.path)
663+
if (stat.isDirectory()) return sendRedirect(path, options)
640664
return sendFileDirectly(request, path, stat, options)
641665
}
642666

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@
2222
"server"
2323
],
2424
"dependencies": {
25+
"@lukeed/ms": "^2.0.2",
2526
"escape-html": "~1.0.3",
2627
"fast-decode-uri-component": "^1.0.1",
27-
"mime": "^3",
28-
"@lukeed/ms": "^2.0.2"
28+
"http-errors": "^2.0.0",
29+
"mime": "^3"
2930
},
3031
"devDependencies": {
3132
"@fastify/pre-commit": "^2.1.0",

test/send.3.test.js

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
'use strict'
2+
3+
const { test } = require('tap')
4+
const http = require('node:http')
5+
const path = require('node:path')
6+
const request = require('supertest')
7+
const { readdir } = require('node:fs/promises')
8+
const send = require('../lib/send').send
9+
10+
const fixtures = path.join(__dirname, 'fixtures')
11+
12+
test('send(file)', function (t) {
13+
t.plan(5)
14+
15+
t.test('file type', function (t) {
16+
t.plan(6)
17+
18+
const app = http.createServer(async function (req, res) {
19+
const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
20+
t.equal(type, 'file')
21+
t.ok(metadata.path)
22+
t.ok(metadata.stat)
23+
t.notOk(metadata.error)
24+
t.notOk(metadata.requestPath)
25+
res.writeHead(statusCode, headers)
26+
stream.pipe(res)
27+
})
28+
29+
request(app)
30+
.get('/name.txt')
31+
.expect('Content-Length', '4')
32+
.expect(200, 'tobi', err => t.error(err))
33+
})
34+
35+
t.test('directory type', function (t) {
36+
t.plan(6)
37+
38+
const app = http.createServer(async function (req, res) {
39+
const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
40+
t.equal(type, 'directory')
41+
t.ok(metadata.path)
42+
t.notOk(metadata.stat)
43+
t.notOk(metadata.error)
44+
t.ok(metadata.requestPath)
45+
res.writeHead(statusCode, headers)
46+
stream.pipe(res)
47+
})
48+
49+
request(app)
50+
.get('/pets')
51+
.expect('Location', '/pets/')
52+
.expect(301, err => t.error(err))
53+
})
54+
55+
t.test('error type', function (t) {
56+
t.plan(6)
57+
58+
const app = http.createServer(async function (req, res) {
59+
const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
60+
t.equal(type, 'error')
61+
t.notOk(metadata.path)
62+
t.notOk(metadata.stat)
63+
t.ok(metadata.error)
64+
t.notOk(metadata.requestPath)
65+
res.writeHead(statusCode, headers)
66+
stream.pipe(res)
67+
})
68+
69+
const path = Array(100).join('foobar')
70+
request(app)
71+
.get('/' + path)
72+
.expect(404, err => t.error(err))
73+
})
74+
75+
t.test('custom directory index view', function (t) {
76+
t.plan(1)
77+
78+
const app = http.createServer(async function (req, res) {
79+
const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
80+
if (type === 'directory') {
81+
const list = await readdir(metadata.path)
82+
res.writeHead(200, { 'Content-Type': 'text/plain; charset=UTF-8' })
83+
res.end(list.join('\n') + '\n')
84+
} else {
85+
res.writeHead(statusCode, headers)
86+
stream.pipe(res)
87+
}
88+
})
89+
90+
request(app)
91+
.get('/pets')
92+
.expect('Content-Type', 'text/plain; charset=UTF-8')
93+
.expect(200, '.hidden\nindex.html\n', err => t.error(err))
94+
})
95+
96+
t.test('serving from a root directory with custom error-handling', function (t) {
97+
t.plan(3)
98+
99+
const app = http.createServer(async function (req, res) {
100+
const { statusCode, headers, stream, type, metadata } = await send(req, req.url, { root: fixtures })
101+
switch (type) {
102+
case 'directory': {
103+
res.writeHead(301, {
104+
Location: metadata.requestPath + '/'
105+
})
106+
res.end('Redirecting to ' + metadata.requestPath + '/')
107+
break
108+
}
109+
case 'error': {
110+
res.writeHead(metadata.error.status ?? 500, {})
111+
res.end(metadata.error.message)
112+
break
113+
}
114+
default: {
115+
// serve all files for download
116+
res.setHeader('Content-Disposition', 'attachment')
117+
res.writeHead(statusCode, headers)
118+
stream.pipe(res)
119+
}
120+
}
121+
})
122+
123+
request(app)
124+
.get('/pets')
125+
.expect('Location', '/pets/')
126+
.expect(301, err => t.error(err))
127+
128+
request(app)
129+
.get('/not-exists')
130+
.expect(404, err => t.error(err))
131+
132+
request(app)
133+
.get('/pets/index.html')
134+
.expect('Content-Disposition', 'attachment')
135+
.expect(200, err => t.error(err))
136+
})
137+
})

0 commit comments

Comments
 (0)