From e2031a1bb8df4a08228d8a126e2da0ae7494611d Mon Sep 17 00:00:00 2001 From: Gil Pedersen Date: Fri, 15 Mar 2024 11:20:57 +0100 Subject: [PATCH] Improve expect 100-continue handling --- lib/route.js | 24 ++++++---- test/payload.js | 118 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 9 deletions(-) diff --git a/lib/route.js b/lib/route.js index 0fb4e0d49..6491693ff 100755 --- a/lib/route.js +++ b/lib/route.js @@ -408,14 +408,15 @@ internals.payload = async function (request) { return; } - if (request._expectContinue) { - request.raw.res.writeContinue(); - } - if (request.payload !== undefined) { return internals.drain(request); } + if (request._expectContinue) { + request._expectContinue = false; + request.raw.res.writeContinue(); + } + try { const { payload, mime } = await Subtext.parse(request.raw.req, request._tap(), request.route.settings.payload); @@ -426,9 +427,7 @@ internals.payload = async function (request) { catch (err) { Bounce.rethrow(err, 'system'); - if (request._isPayloadPending) { - await internals.drain(request); - } + await internals.drain(request); request.mime = err.mime; request.payload = null; @@ -442,8 +441,15 @@ internals.drain = async function (request) { // Flush out any pending request payload not consumed due to errors - await Streams.drain(request.raw.req); - request._isPayloadPending = false; + if (request._expectContinue) { + request._isPayloadPending = false; // If we don't continue, client should not send a payload + request._expectContinue = false; + } + + if (request._isPayloadPending) { + await Streams.drain(request.raw.req); + request._isPayloadPending = false; + } }; diff --git a/test/payload.js b/test/payload.js index 0cfadedd6..93b045cdf 100755 --- a/test/payload.js +++ b/test/payload.js @@ -7,6 +7,7 @@ const Net = require('net'); const Path = require('path'); const Zlib = require('zlib'); +const Boom = require('@hapi/boom'); const Code = require('@hapi/code'); const Hapi = require('..'); const Hoek = require('@hapi/hoek'); @@ -309,6 +310,123 @@ describe('Payload', () => { await server.stop(); }); + it('does not continue on errors before payload processing', async () => { + + const server = Hapi.server(); + server.route({ method: 'POST', path: '/', handler: (request) => request.payload }); + server.ext('onPreAuth', (request, h) => { + + throw new Boom.forbidden(); + }); + + await server.start(); + + const client = Net.connect(server.info.port); + + await Events.once(client, 'connect'); + + client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' + + 'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n'); + + let continued = false; + const lines = []; + client.setEncoding('ascii'); + for await (const chunk of client) { + + if (chunk.startsWith('HTTP/1.1 100 Continue')) { + client.write('{"hello":true}'); + continued = true; + } + else { + lines.push(...chunk.split('\r\n')); + } + } + + const res = lines.shift(); + + expect(res).to.equal('HTTP/1.1 403 Forbidden'); + expect(continued).to.be.false(); + + await server.stop(); + }); + + it('handles expect 100-continue on undefined routes', async () => { + + const server = Hapi.server(); + await server.start(); + + const client = Net.connect(server.info.port); + + await Events.once(client, 'connect'); + + client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' + + 'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n'); + + let continued = false; + const lines = []; + client.setEncoding('ascii'); + for await (const chunk of client) { + + if (chunk.startsWith('HTTP/1.1 100 Continue')) { + client.write('{"hello":true}'); + continued = true; + } + else { + lines.push(...chunk.split('\r\n')); + } + } + + const res = lines.shift(); + + expect(res).to.equal('HTTP/1.1 404 Not Found'); + expect(continued).to.be.false(); + + await server.stop(); + }); + + it('does not continue on custom request.payload', async () => { + + const server = Hapi.server(); + server.route({ method: 'POST', path: '/', handler: (request) => request.payload }); + server.ext('onRequest', (request, h) => { + + request.payload = { custom: true }; + return h.continue; + }); + + await server.start(); + + const client = Net.connect(server.info.port); + + await Events.once(client, 'connect'); + + client.write('POST / HTTP/1.1\r\nexpect: 100-continue\r\nhost: host\r\naccept-encoding: gzip\r\n' + + 'content-type: application/json\r\ncontent-length: 14\r\nConnection: close\r\n\r\n'); + + let continued = false; + const lines = []; + client.setEncoding('ascii'); + for await (const chunk of client) { + + if (chunk.startsWith('HTTP/1.1 100 Continue')) { + client.write('{"hello":true}'); + continued = true; + } + else { + lines.push(...chunk.split('\r\n')); + } + } + + const res = lines.shift(); + const payload = lines.pop(); + + expect(res).to.equal('HTTP/1.1 200 OK'); + expect(payload).to.equal('{"custom":true}'); + expect(continued).to.be.false(); + + await server.stop(); + }); + it('peeks at unparsed data', async () => { let data = null;