Commit 979979d2 authored by Sebastiaan Deckers's avatar Sebastiaan Deckers 馃悜

feat: hosts[].fallback.extensions option

parent a4b3a4a7
......@@ -56,9 +56,7 @@ If no directory is auto-detected, the current working directory is used. A warni
Default: `{}` (no fallback)
An object mapping HTTP status code to file paths.
Typically used for client side routing.
An object mapping HTTP status code to file paths. The file path is served as response body. No HTTP redirect is used. Typically used for client-side routing or fancy `404` error pages.
Fallbacks are supported for:
......@@ -85,6 +83,38 @@ module.exports = {
}
```
### `hosts[].fallback.extensions`
Default: `['', 'html']`
An array of file extensions for which to allow the `200` fallback. This prevents client-side apps from serving a fallback page on broken resource links. The extension is parsed from the request URL path. The empty extension `''` must be explicitly included to allow fallback on a path without file extension. Fallbacks on paths with a trailing slash are always allowed. An empty array allows any path, regardless of extension, to have the `200` fallback.
Example for client-side routing of *all* paths:
```js
module.exports = {
hosts: [{
fallback: {
200: './app.html',
extensions: []
}
}]
}
```
Example for client-side routing of specific file types:
```js
module.exports = {
hosts: [{
fallback: {
200: './app.html',
extensions: ['md', 'html', 'htm', 'php']
}
}]
}
```
## `hosts[].directories`
Behaviour of directory paths in *clean URLs*.
......
......@@ -8,8 +8,6 @@ const UriTemplate = require('uri-templates')
const PERMANENT_REDIRECT = 308
const fallbackExtensions = ['', '.html', '.htm']
module.exports.resolveRequest = () => {
return function resolveRequest (request, response, next) {
const url = parseUrl(request)
......@@ -54,9 +52,11 @@ module.exports.resolveRequest = () => {
const fallback = request.options.fallback['200']
if (fileIndex.absolute.has(fallback)) {
const { extensions } = request.options.fallback
if (
hasTrailingSlash ||
fallbackExtensions.includes(extname(pathname))
extensions.length === 0 ||
extensions.includes(extname(pathname).substr(1))
) {
const accepted = accepts(request)
if (accepted.type('text/html')) {
......
const test = require('blue-tape')
const { Master } = require('..')
const { join } = require('path')
const { connect } = require('http2')
const { StringDecoder } = require('string_decoder')
const concat = require('concat-stream')
const { h2 } = require('./helpers/receive')
let master
test('start server', async (t) => {
const cwd = join(__dirname, 'fixtures')
const options = {
hosts: [
{
domain: 'fallback-200',
fallback: { 200: '/200.html' }
},
{
domain: 'fallback-404',
fallback: { 404: '/404.html' }
}
]
const fixtures = [
{
host: { fallback: { 200: '/200.html' } },
requests: new Map([
[
'Serve fallback on missing extension',
{ path: '/foobar', status: 200, payload: 'Fallback found' }
],
[
'Serve fallback on HTML file',
{ path: '/foobar.html', status: 200, payload: 'Fallback found' }
],
[
'Serve fallback on directory',
{ path: '/foobar', status: 200, payload: 'Fallback found' }
],
[
'Serve 404 error on generic file extension',
{ path: '/foo.bar', status: 404, payload: 'Not Found' }
]
])
},
{
host: { fallback: { 404: '/404.html' } },
requests: new Map([
[
'Serve 404 fallback on missing extension',
{ path: '/foobar', status: 404, payload: 'Fallback missing' }
],
[
'Serve 404 fallback on HTML file',
{ path: '/foobar.html', status: 404, payload: 'Fallback missing' }
],
[
'Serve 404 fallback on directory',
{ path: '/foobar', status: 404, payload: 'Fallback missing' }
],
[
'Serve 404 fallback on generic file extension',
{ path: '/foo.bar', status: 404, payload: 'Fallback missing' }
]
])
},
{
host: { fallback: { 200: '/200.html', extensions: [] } },
requests: new Map([
[
'Serve 200 fallback for any extension',
{ path: '/foo.bar', status: 200, payload: 'Fallback found' }
],
[
'Serve 200 fallback for no extension',
{ path: '/foobar', status: 200, payload: 'Fallback found' }
],
[
'Serve 200 fallback for trailing slash directory',
{ path: '/foobar/', status: 200, payload: 'Fallback found' }
]
])
}
master = new Master({ cwd, options })
await master.listen()
})
]
const fixtures = {
200: new Map([
[
'Serve fallback on missing extension',
{ path: '/foobar', status: 200, payload: 'Fallback found' }
],
[
'Serve fallback on HTML file',
{ path: '/foobar.html', status: 200, payload: 'Fallback found' }
],
[
'Serve fallback on directory',
{ path: '/foobar', status: 200, payload: 'Fallback found' }
],
[
'Serve 404 error on generic file extension',
{ path: '/foo.bar', status: 404, payload: 'Not Found' }
]
]),
404: new Map([
[
'Serve 404 fallback on missing extension',
{ path: '/foobar', status: 404, payload: 'Fallback missing' }
],
[
'Serve 404 fallback on HTML file',
{ path: '/foobar.html', status: 404, payload: 'Fallback missing' }
],
[
'Serve 404 fallback on directory',
{ path: '/foobar', status: 404, payload: 'Fallback missing' }
],
[
'Serve 404 fallback on generic file extension',
{ path: '/foo.bar', status: 404, payload: 'Fallback missing' }
]
])
}
for (const key of Object.keys(fixtures)) {
for (const [label, { path, status, payload }] of fixtures[key]) {
test(label, (t) => {
const session = connect(
`https://fallback-${key}:8443`,
{
rejectUnauthorized: false,
lookup: (hostname, options, callback) => {
callback(null, '127.0.0.1', 4)
}
}
)
const stream = session.request({ ':path': path })
stream.on('socketError', t.end)
stream.on('error', t.end)
for (const { host, requests } of fixtures) {
let master
test('start server', async (t) => {
const cwd = join(__dirname, 'fixtures')
const options = { hosts: [ host ] }
master = new Master({ cwd, options })
await master.listen()
})
stream.on('response', (headers) => {
t.is(headers[':status'], status)
// stream.resume()
// stream.on('end', () => stream.close())
stream.pipe(concat((data) => {
const body = new StringDecoder().end(data)
t.is(body, payload)
stream.close()
}))
stream.on('close', () => session.close(t.end))
})
for (const [label, { path, status, payload }] of requests) {
test(label, async (t) => {
const url = `https://localhost:8443${path}`
const response = await h2(url)
t.notOk(response.headers.get('non-ascii'))
t.is(response.headers.get(':status'), status)
t.is(await response.text(), payload)
})
}
}
test('stop server', async (t) => master.close())
test('stop server', async (t) => master.close())
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment