Skip to content

Commit b4f06c4

Browse files
committed
Fix mount for Node.js >= 15.1.0
I tried the basic example in the README. It failed with a 500 Internal Server Error and the following JSON body: { "message": "Cannot read properties of undefined (reading 'x-forwarded-proto')", "name": "TypeError" } The output from the server process includes a stack trace. Sanitized a bit, it is: TypeError: Cannot read properties of undefined (reading 'x-forwarded-proto') at protocol (.../node_modules/paperplane/lib/parseUrl.js:18:14) at .../node_modules/ramda/src/converge.js:47:17 at _map (.../node_modules/ramda/src/internal/_map.js:6:19) at .../node_modules/ramda/src/converge.js:46:33 at f1 (.../node_modules/ramda/src/internal/_curry1.js:18:17) at .../node_modules/ramda/src/uncurryN.js:34:21 at .../node_modules/ramda/src/internal/_curryN.js:37:27 at .../node_modules/ramda/src/internal/_arity.js:10:19 at .../node_modules/ramda/src/internal/_pipe.js:3:14 at .../node_modules/ramda/src/internal/_arity.js:10:19 Yet the docker-based demo app works. That uses Node.js v12.22.12. When I tried that Node version on the basic example, it also worked. So, I used `nvm` to do a binary search on versions. I identified that Node.js v15.1.0 is the first failing version, and every version I tried that was newer (up to v21.2.0) also failed. Scouring the Node.js change log revealed that the http module of v15.1.0 started calculating req.headers lazily. There's a full discussion in nodejs/node#35281 [1]. Note the referenced issue that identifies the bug [2]. [1] nodejs/node#35281 [2] nodejs/node#36550 The workaround identified in this comment [3] is not to use the spread operator or `Object.assign` on the request object. "These objects are essentially uncloneable." [3] nodejs/node#36550 (comment) This is surprisingly tricky to do right. Various Ramda functions like `merge` (and I think `converge`) do this implicitly, as does funky's `assocWith`. So to work around this limitation, while preserving all behavior, I had to resort to (gasp) mutating procedures instead of pure functions. Not ideal, I know. I'm open to better ideas. So, maybe this isn't the best solution, but it least it gets the example working again on modern Node versions. If it never gets merged, at least it will be findable via the repo on GitHub. For reference, to test this, I used a fresh NPM project with only the paperplane dependency: mkdir paperplane cd paperplane npm init -y npm i -S paperplane In which I added an index.js file with these contents: const { compose } = require('ramda') const http = require('http') const { json, logger, methods, mount, routes } = require('paperplane') const cookies = req => ({ cookies: req.cookies || 'none?' }) const hello = req => ({ message: `Hello ${req.params.name}!` }) const app = routes({ '/' : methods({ GET: _ => ({ body: 'Hello anon.' }) }), '/cookies' : methods({ GET: compose(json, cookies) }), '/hello/:name': methods({ GET: compose(json, hello) }) }) http.createServer(mount({ app })).listen(3000, logger) And finally (with `fd` and `entr` and `httpie` installed), I started a file-watching auto-test process: fd -e js | entr -rcc bash -c "node index.js & http -v get http://localhost:3000/cookies Cookie:foo=bar"
1 parent ef30068 commit b4f06c4

File tree

3 files changed

+28
-32
lines changed

3 files changed

+28
-32
lines changed

lib/mount.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,12 @@ const { wrap } = require('./wrap')
1212
const { writeHTTP } = require('./writeHTTP')
1313
const { writeLambda } = require('./writeLambda')
1414

15-
const keepOriginal = curry((response, request) => merge(request, {
16-
original: request, // legacy
17-
request,
18-
response
19-
}))
15+
const keepOriginal = res => req => {
16+
req.original = req
17+
req.request = req
18+
req.response = res
19+
return req
20+
}
2021

2122
exports.mount = (opts={}) => {
2223
const {

lib/parseUrl.js

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
1-
const { assocWith } = require('@articulate/funky')
21
const qs = require('qs')
32
const url = require('url')
43

5-
const {
6-
converge, evolve, identity, merge, pick, pipe, prop
7-
} = require('ramda')
4+
const protocol = ({ headers, socket }) =>
5+
headers['x-forwarded-proto'] ||
6+
socket.encrypted ? 'https' : 'http'
87

9-
const pathParts =
10-
pipe(
11-
prop('url'),
12-
url.parse,
13-
evolve({ query: qs.parse }),
14-
pick(['pathname', 'query'])
15-
)
8+
const pathParts = req => {
9+
const { pathname, query } = url.parse(req.url)
10+
return {
11+
pathname,
12+
query: qs.parse(query),
13+
protocol: protocol(req),
14+
}
15+
}
1616

17-
const protocol = req =>
18-
req.headers['x-forwarded-proto'] ||
19-
req.socket.encrypted ? 'https' : 'http'
20-
21-
exports.parseUrl =
22-
pipe(
23-
converge(merge, [ identity, pathParts ]),
24-
assocWith('protocol', protocol)
25-
)
17+
exports.parseUrl = req => {
18+
const { pathname, query, protocol } = pathParts(req)
19+
req.pathname = pathname
20+
req.query = query
21+
req.protocol = protocol
22+
return req
23+
}

lib/passThruBody.js

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
const { assocWith } = require('@articulate/funky')
21
const { PassThrough } = require('stream')
32

4-
const passThru = ({ original }) => {
3+
exports.passThruBody = req => {
54
const body = new PassThrough()
6-
original.pipe(body)
7-
return body
5+
req.original.pipe(body)
6+
req.body = body
7+
return req
88
}
9-
10-
exports.passThruBody =
11-
assocWith('body', passThru)

0 commit comments

Comments
 (0)