-
Notifications
You must be signed in to change notification settings - Fork 29.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
http: add http.createStaticServer
#45096
base: main
Are you sure you want to change the base?
Changes from all commits
318610e
77e0416
b1cb361
ac549c4
d3b189f
b6dbf4f
bc6fd54
39dbb48
c53b51e
c394473
6502cbc
9d408e7
9a9caa7
bd9a6bd
a0c7864
20e3d86
98a5563
1259d43
f3d663d
60d3f0b
41fc901
47d2f8b
fa52ee0
968efde
5e018ef
3698f0c
d8ce4b3
0c91db4
7b40455
0bc9f5e
29257f9
a5ea0d7
c1e6869
2e63aba
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
'use strict'; | ||
|
||
const { | ||
Promise, | ||
RegExpPrototypeExec, | ||
StringPrototypeEndsWith, | ||
StringPrototypeIndexOf, | ||
StringPrototypeLastIndexOf, | ||
StringPrototypeSlice, | ||
StringPrototypeStartsWith, | ||
StringPrototypeToLocaleLowerCase, | ||
} = primordials; | ||
|
||
const { cwd } = process; | ||
const console = require('internal/console/global'); | ||
const { createReadStream } = require('fs'); | ||
const { Server } = require('_http_server'); | ||
const { URL, isURL, pathToFileURL } = require('internal/url'); | ||
const { emitExperimentalWarning, kEmptyObject } = require('internal/util'); | ||
const { validateFunction, validatePort } = require('internal/validators'); | ||
|
||
const mimeDefault = { | ||
'__proto__': null, | ||
'.html': 'text/html; charset=UTF-8', | ||
'.js': 'text/javascript; charset=UTF-8', | ||
'.css': 'text/css; charset=UTF-8', | ||
'.avif': 'image/avif', | ||
'.svg': 'image/svg+xml', | ||
'.json': 'application/json', | ||
'.jpg': 'image/jpeg', | ||
'.jpeg': 'image/jpeg', | ||
'.png': 'image/png', | ||
'.wasm': 'application/wasm', | ||
'.webp': 'image/webp', | ||
'.woff2': 'font/woff2', | ||
}; | ||
|
||
const dot = /\/(\.|%2[eE])/; | ||
function filterDotFiles(url, fileURL) { | ||
return RegExpPrototypeExec(dot, fileURL.pathname) === null; | ||
} | ||
|
||
function createStaticServer(options = kEmptyObject) { | ||
emitExperimentalWarning('http/static'); | ||
|
||
const { | ||
directory = cwd(), | ||
port, | ||
host = 'localhost', | ||
mimeOverrides, | ||
filter = filterDotFiles, | ||
log = console.log, | ||
onStart = (host, port) => console.log(`Server started on http://${host}:${port}`), | ||
} = options; | ||
const mime = mimeOverrides ? { | ||
__proto__: null, | ||
...mimeDefault, | ||
...mimeOverrides, | ||
} : mimeDefault; | ||
|
||
const directoryURL = isURL(directory) ? directory : pathToFileURL(directory); | ||
// To be used as a base URL, it is necessary that the URL ends with a slash: | ||
const baseDirectoryURL = StringPrototypeEndsWith(directoryURL.pathname, '/') ? | ||
directoryURL : new URL(`${directoryURL}/`); | ||
|
||
if (port != null) validatePort(port); | ||
if (filter != null) validateFunction(filter, 'options.filter'); | ||
|
||
const server = new Server(async (req, res) => { | ||
const url = new URL( | ||
req.url === '/' || StringPrototypeStartsWith(req.url, '/?') ? | ||
'./index.html' : | ||
'.' + StringPrototypeSlice(`${new URL(req.url, 'root://')}`, 6), | ||
baseDirectoryURL, | ||
); | ||
|
||
if (filter != null && !filter(req.url, url)) { | ||
log?.(403, req.url); | ||
res.statusCode = 403; | ||
res.end('Forbidden\n'); | ||
return; | ||
} | ||
|
||
const ext = StringPrototypeToLocaleLowerCase( | ||
StringPrototypeSlice(url.pathname, StringPrototypeLastIndexOf(url.pathname, '.')), | ||
); | ||
if (ext in mime) res.setHeader('Content-Type', mime[ext]); | ||
H4ad marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
try { | ||
try { | ||
const stream = createReadStream(url, { emitClose: false }); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know is a nitpick but I think you can cache this object to avoid creating every time. Also, maybe expose the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I understand what you mean, do you mean cache the stream? We're going to need a different stream for each request, I think, so I don't see how it'd be useful 🤔 could you send an example maybe? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Caching |
||
await new Promise((resolve, reject) => { | ||
stream.on('error', reject); | ||
stream.on('end', resolve); | ||
stream.pipe(res); | ||
}); | ||
log?.(200, req.url); | ||
} catch (err) { | ||
if (err?.code === 'EISDIR') { | ||
if (StringPrototypeEndsWith(req.url, '/') || RegExpPrototypeExec(/^[^?]+\/\?/, req.url) !== null) { | ||
const stream = createReadStream(new URL('./index.html', url), { | ||
emitClose: false, | ||
}); | ||
res.setHeader('Content-Type', mime['.html']); | ||
await new Promise((resolve, reject) => { | ||
stream.on('error', reject); | ||
stream.on('end', resolve); | ||
stream.pipe(res); | ||
}); | ||
log?.(200, req.url); | ||
} else { | ||
log?.(307, req.url); | ||
res.statusCode = 307; | ||
const index = StringPrototypeIndexOf(req.url, '?'); | ||
res.setHeader( | ||
'Location', | ||
index === -1 ? | ||
`${req.url}/` : | ||
`${StringPrototypeSlice(req.url, 0, index)}/${StringPrototypeSlice(req.url, index)}`, | ||
); | ||
res.end('Temporary Redirect\n'); | ||
} | ||
} else { | ||
throw err; | ||
} | ||
} | ||
} catch (err) { | ||
if (err?.code === 'ENOENT') { | ||
log?.(404, req.url); | ||
res.statusCode = 404; | ||
res.end('Not Found\n'); | ||
} else { | ||
log?.(500, req.url, err); | ||
res.statusCode = 500; | ||
res.end('Internal Error\n'); | ||
} | ||
} | ||
}); | ||
const callback = () => { | ||
const { address, family, port } = server.address(); | ||
const host = family === 'IPv6' ? `[${address}]` : address; | ||
onStart?.(host, port); | ||
}; | ||
if (host != null) { | ||
server.listen(port, host, callback); | ||
} else if (port != null) { | ||
server.listen(port, callback); | ||
} else { | ||
server.listen(callback); | ||
} | ||
|
||
return server; | ||
} | ||
|
||
module.exports = createStaticServer; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Content of .bar |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Content of .foo/bar.js |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Dummy file to test mimeOverrides option |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Content of subfolder/index.html |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
Content of test.html |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Or "should" / "must"? /cc @nodejs/documentation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
^