Aktueller Stand

This commit is contained in:
2026-01-22 19:05:45 +01:00
parent 85dee61a4d
commit e280e4eadb
1967 changed files with 397327 additions and 74093 deletions

View File

@@ -9,5 +9,5 @@ updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
interval: "monthly"
open-pull-requests-limit: 10

View File

@@ -2,19 +2,19 @@ name: Benchmark PR
on:
pull_request_target:
types:
types:
- labeled
jobs:
benchmark:
if: ${{ github.event.label.name == 'benchmark' }}
uses: fastify/workflows/.github/workflows/plugins-benchmark-pr.yml@main
uses: fastify/workflows/.github/workflows/plugins-benchmark-pr.yml@v5
with:
npm-script: benchmark
remove-label:
if: "always()"
needs:
needs:
- benchmark
runs-on: ubuntu-latest
steps:

View File

@@ -4,7 +4,6 @@ on:
push:
branches:
- main
- master
- next
- 'v*'
paths-ignore:
@@ -17,7 +16,7 @@ on:
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
fastify-dependency-integration: true
license-check: true

View File

@@ -1,2 +0,0 @@
files:
- test/**/*.test.js

View File

@@ -1,8 +1,8 @@
# Light my Request
![CI](https://github.com/fastify/light-my-request/workflows/CI/badge.svg)
[![CI](https://github.com/fastify/light-my-request/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/fastify/light-my-request/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/light-my-request.svg?style=flat)](https://www.npmjs.com/package/light-my-request)
[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
Injects a fake HTTP request/response into a node HTTP server for simulating server logic, writing tests, or debugging.
Does not use a socket connection so can be run against an inactive server (server not in listen mode).
@@ -153,7 +153,7 @@ Injects a fake request into an HTTP server.
- `headers` - an optional object containing request headers.
- `cookies` - an optional object containing key-value pairs that will be encoded and added to `cookie` header. If the header is already set, the data will be appended.
- `remoteAddress` - an optional string specifying the client remote address. Defaults to `'127.0.0.1'`.
- `payload` - an optional request payload. Can be a string, Buffer, Stream or object. If the payload is string, Buffer or Stream is used as is as the request payload. Oherwise it is serialized with `JSON.stringify` forcing the request to have the `Content-type` equal to `application/json`
- `payload` - an optional request payload. Can be a string, Buffer, Stream, or object. If the payload is string, Buffer or Stream is used as is as the request payload. Otherwise, it is serialized with `JSON.stringify` forcing the request to have the `Content-type` equal to `application/json`
- `query` - an optional object or string containing query parameters.
- `body` - alias for payload.
- `simulate` - an object containing flags to simulate various conditions:
@@ -168,6 +168,7 @@ Injects a fake request into an HTTP server.
- `signal` - An `AbortSignal` that may be used to abort an ongoing request. Requires Node v16+.
- `Request` - Optional type from which the `request` object should inherit
instead of `stream.Readable`
- `payloadAsStream` - if set to `true`, the response will be streamed and not accumulated; in this case `res.payload`, `res.rawPayload` will be undefined.
- `callback` - the callback function using the signature `function (err, res)` where:
- `err` - error object
- `res` - a response object where:
@@ -208,11 +209,11 @@ Checks if given object `obj` is a *light-my-request* `Request` object.
#### Method chaining
There are following methods you can used as chaining:
The following methods can be used in chaining:
- `delete`, `get`, `head`, `options`, `patch`, `post`, `put`, `trace`. They will set the HTTP request method and the request URL.
- `body`, `headers`, `payload`, `query`, `cookies`. They can be used to set the request options object.
And finally you need to call `end`. It has the signature `function (callback)`.
And finally, you need to call `end`. It has the signature `function (callback)`.
If you invoke `end` without a callback function, the method will return a promise, thus you can:
```js
@@ -248,12 +249,12 @@ inject(dispatch)
})
```
Note: The application would not respond multiple times. If you try to invoking any method after the application has responded, the application would throw an error.
Note: The application would not respond multiple times. If you try to invoke any method after the application has responded, the application would throw an error.
## Acknowledgements
## Acknowledgments
This project has been forked from [`hapi/shot`](https://github.com/hapijs/shot) because we wanted to support *Node ≥ v4* and not only *Node ≥ v8*.
All the credits before the commit [00a2a82](https://github.com/fastify/light-my-request/commit/00a2a82eb773b765003b6085788cc3564cd08326) goes to the `hapi/shot` project [contributors](https://github.com/hapijs/shot/graphs/contributors).
Since the commit [db8bced](https://github.com/fastify/light-my-request/commit/db8bced10b4367731688c8738621d42f39680efc) the project will be maintained by the Fastify team.
All credits prior to commit [00a2a82](https://github.com/fastify/light-my-request/commit/00a2a82eb773b765003b6085788cc3564cd08326) go to the `hapi/shot` project [contributors](https://github.com/hapijs/shot/graphs/contributors).
Since commit [db8bced](https://github.com/fastify/light-my-request/commit/db8bced10b4367731688c8738621d42f39680efc) the project will be maintained by the Fastify team.
## License

View File

@@ -5,8 +5,8 @@ const Request = require('../lib/request')
const Response = require('../lib/response')
const inject = require('..')
const parseURL = require('../lib/parse-url')
const { Readable } = require('stream')
const { assert } = require('console')
const { Readable } = require('node:stream')
const { assert } = require('node:console')
const { Bench } = require('tinybench')
const suite = new Bench()

View File

@@ -75,7 +75,7 @@ const factory = AjvStandaloneCompiler({
readMode: false,
storeFunction (routeOpts, schemaValidationCode) {
const moduleCode = `// This file is autogenerated by ${__filename.replace(__dirname, 'build')}, do not edit
/* istanbul ignore file */
/* c8 ignore start */
/* eslint-disable */
${schemaValidationCode}
`

View File

@@ -0,0 +1,9 @@
'use strict'
module.exports = require('neostandard')({
ignores: [
...require('neostandard').resolveIgnoresFromGitignore(),
'test/benchmark.js'
],
ts: true
})

View File

@@ -16,16 +16,43 @@ function inject (dispatchFunc, options, callback) {
}
}
function supportStream1 (req, next) {
const payload = req._lightMyRequest.payload
if (!payload || payload._readableState || typeof payload.resume !== 'function') { // does quack like a modern stream
return next()
}
// This is a non-compliant stream
const chunks = []
// We are accumulating because Readable.wrap() does not really work as expected
// in this case.
payload.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
payload.on('end', () => {
const payload = Buffer.concat(chunks)
req.headers['content-length'] = req.headers['content-length'] || ('' + payload.length)
delete req.headers['transfer-encoding']
req._lightMyRequest.payload = payload
return next()
})
// Force to resume the stream. Needed for Stream 1
payload.resume()
}
function makeRequest (dispatchFunc, server, req, res) {
req.once('error', function (err) {
if (this.destroyed) res.destroy(err)
})
req.once('close', function () {
if (this.destroyed && !this._error) res.destroy()
if (this.destroyed && !this._error) {
res.destroy()
}
})
return req.prepare(() => dispatchFunc.call(server, req, res))
return supportStream1(req, () => dispatchFunc.call(server, req, res))
}
function doInject (dispatchFunc, options, callback) {
@@ -157,7 +184,7 @@ function isInjection (obj) {
return (
obj instanceof Request ||
obj instanceof Response ||
(obj && obj.constructor && obj.constructor.name === '_CustomLMRRequest')
obj?.constructor?.name === '_CustomLMRRequest'
)
}

View File

@@ -1,10 +1,10 @@
// This file is autogenerated by build/build-validation.js, do not edit
/* istanbul ignore file */
/* c8 ignore start */
/* eslint-disable */
"use strict";
module.exports = validate10;
module.exports.default = validate10;
const schema11 = {"type":"object","properties":{"url":{"oneOf":[{"type":"string"},{"type":"object","properties":{"protocol":{"type":"string"},"hostname":{"type":"string"},"pathname":{"type":"string"}},"additionalProperties":true,"required":["pathname"]}]},"path":{"oneOf":[{"type":"string"},{"type":"object","properties":{"protocol":{"type":"string"},"hostname":{"type":"string"},"pathname":{"type":"string"}},"additionalProperties":true,"required":["pathname"]}]},"cookies":{"type":"object","additionalProperties":true},"headers":{"type":"object","additionalProperties":true},"query":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"string"}]},"simulate":{"type":"object","properties":{"end":{"type":"boolean"},"split":{"type":"boolean"},"error":{"type":"boolean"},"close":{"type":"boolean"}}},"authority":{"type":"string"},"remoteAddress":{"type":"string"},"method":{"type":"string","enum":["ACL","BIND","CHECKOUT","CONNECT","COPY","DELETE","GET","HEAD","LINK","LOCK","M-SEARCH","MERGE","MKACTIVITY","MKCALENDAR","MKCOL","MOVE","NOTIFY","OPTIONS","PATCH","POST","PROPFIND","PROPPATCH","PURGE","PUT","REBIND","REPORT","SEARCH","SOURCE","SUBSCRIBE","TRACE","UNBIND","UNLINK","UNLOCK","UNSUBSCRIBE","acl","bind","checkout","connect","copy","delete","get","head","link","lock","m-search","merge","mkactivity","mkcalendar","mkcol","move","notify","options","patch","post","propfind","proppatch","purge","put","rebind","report","search","source","subscribe","trace","unbind","unlink","unlock","unsubscribe"]},"validate":{"type":"boolean"}},"additionalProperties":true,"oneOf":[{"required":["url"]},{"required":["path"]}]};
const schema11 = {"type":"object","properties":{"url":{"oneOf":[{"type":"string"},{"type":"object","properties":{"protocol":{"type":"string"},"hostname":{"type":"string"},"pathname":{"type":"string"}},"additionalProperties":true,"required":["pathname"]}]},"path":{"oneOf":[{"type":"string"},{"type":"object","properties":{"protocol":{"type":"string"},"hostname":{"type":"string"},"pathname":{"type":"string"}},"additionalProperties":true,"required":["pathname"]}]},"cookies":{"type":"object","additionalProperties":true},"headers":{"type":"object","additionalProperties":true},"query":{"anyOf":[{"type":"object","additionalProperties":true},{"type":"string"}]},"simulate":{"type":"object","properties":{"end":{"type":"boolean"},"split":{"type":"boolean"},"error":{"type":"boolean"},"close":{"type":"boolean"}}},"authority":{"type":"string"},"remoteAddress":{"type":"string"},"method":{"type":"string","enum":["ACL","BIND","CHECKOUT","CONNECT","COPY","DELETE","GET","HEAD","LINK","LOCK","M-SEARCH","MERGE","MKACTIVITY","MKCALENDAR","MKCOL","MOVE","NOTIFY","OPTIONS","PATCH","POST","PROPFIND","PROPPATCH","PURGE","PUT","QUERY","REBIND","REPORT","SEARCH","SOURCE","SUBSCRIBE","TRACE","UNBIND","UNLINK","UNLOCK","UNSUBSCRIBE","acl","bind","checkout","connect","copy","delete","get","head","link","lock","m-search","merge","mkactivity","mkcalendar","mkcol","move","notify","options","patch","post","propfind","proppatch","purge","put","query","rebind","report","search","source","subscribe","trace","unbind","unlink","unlock","unsubscribe"]},"validate":{"type":"boolean"}},"additionalProperties":true,"oneOf":[{"required":["url"]},{"required":["path"]}]};
function validate10(data, {instancePath="", parentData, parentDataProperty, rootData=data}={}){
let vErrors = null;
@@ -859,7 +859,7 @@ data["method"] = coerced15;
}
}
}
if(!((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((data18 === "ACL") || (data18 === "BIND")) || (data18 === "CHECKOUT")) || (data18 === "CONNECT")) || (data18 === "COPY")) || (data18 === "DELETE")) || (data18 === "GET")) || (data18 === "HEAD")) || (data18 === "LINK")) || (data18 === "LOCK")) || (data18 === "M-SEARCH")) || (data18 === "MERGE")) || (data18 === "MKACTIVITY")) || (data18 === "MKCALENDAR")) || (data18 === "MKCOL")) || (data18 === "MOVE")) || (data18 === "NOTIFY")) || (data18 === "OPTIONS")) || (data18 === "PATCH")) || (data18 === "POST")) || (data18 === "PROPFIND")) || (data18 === "PROPPATCH")) || (data18 === "PURGE")) || (data18 === "PUT")) || (data18 === "REBIND")) || (data18 === "REPORT")) || (data18 === "SEARCH")) || (data18 === "SOURCE")) || (data18 === "SUBSCRIBE")) || (data18 === "TRACE")) || (data18 === "UNBIND")) || (data18 === "UNLINK")) || (data18 === "UNLOCK")) || (data18 === "UNSUBSCRIBE")) || (data18 === "acl")) || (data18 === "bind")) || (data18 === "checkout")) || (data18 === "connect")) || (data18 === "copy")) || (data18 === "delete")) || (data18 === "get")) || (data18 === "head")) || (data18 === "link")) || (data18 === "lock")) || (data18 === "m-search")) || (data18 === "merge")) || (data18 === "mkactivity")) || (data18 === "mkcalendar")) || (data18 === "mkcol")) || (data18 === "move")) || (data18 === "notify")) || (data18 === "options")) || (data18 === "patch")) || (data18 === "post")) || (data18 === "propfind")) || (data18 === "proppatch")) || (data18 === "purge")) || (data18 === "put")) || (data18 === "rebind")) || (data18 === "report")) || (data18 === "search")) || (data18 === "source")) || (data18 === "subscribe")) || (data18 === "trace")) || (data18 === "unbind")) || (data18 === "unlink")) || (data18 === "unlock")) || (data18 === "unsubscribe"))){
if(!((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((((data18 === "ACL") || (data18 === "BIND")) || (data18 === "CHECKOUT")) || (data18 === "CONNECT")) || (data18 === "COPY")) || (data18 === "DELETE")) || (data18 === "GET")) || (data18 === "HEAD")) || (data18 === "LINK")) || (data18 === "LOCK")) || (data18 === "M-SEARCH")) || (data18 === "MERGE")) || (data18 === "MKACTIVITY")) || (data18 === "MKCALENDAR")) || (data18 === "MKCOL")) || (data18 === "MOVE")) || (data18 === "NOTIFY")) || (data18 === "OPTIONS")) || (data18 === "PATCH")) || (data18 === "POST")) || (data18 === "PROPFIND")) || (data18 === "PROPPATCH")) || (data18 === "PURGE")) || (data18 === "PUT")) || (data18 === "QUERY")) || (data18 === "REBIND")) || (data18 === "REPORT")) || (data18 === "SEARCH")) || (data18 === "SOURCE")) || (data18 === "SUBSCRIBE")) || (data18 === "TRACE")) || (data18 === "UNBIND")) || (data18 === "UNLINK")) || (data18 === "UNLOCK")) || (data18 === "UNSUBSCRIBE")) || (data18 === "acl")) || (data18 === "bind")) || (data18 === "checkout")) || (data18 === "connect")) || (data18 === "copy")) || (data18 === "delete")) || (data18 === "get")) || (data18 === "head")) || (data18 === "link")) || (data18 === "lock")) || (data18 === "m-search")) || (data18 === "merge")) || (data18 === "mkactivity")) || (data18 === "mkcalendar")) || (data18 === "mkcol")) || (data18 === "move")) || (data18 === "notify")) || (data18 === "options")) || (data18 === "patch")) || (data18 === "post")) || (data18 === "propfind")) || (data18 === "proppatch")) || (data18 === "purge")) || (data18 === "put")) || (data18 === "query")) || (data18 === "rebind")) || (data18 === "report")) || (data18 === "search")) || (data18 === "source")) || (data18 === "subscribe")) || (data18 === "trace")) || (data18 === "unbind")) || (data18 === "unlink")) || (data18 === "unlock")) || (data18 === "unsubscribe"))){
validate10.errors = [{instancePath:instancePath+"/method",schemaPath:"#/properties/method/enum",keyword:"enum",params:{allowedValues: schema11.properties.method.enum},message:"must be equal to one of the allowed values"}];
return false;
}

View File

@@ -53,15 +53,13 @@ function formDataToStream (formdata) {
// header
yield textEncoder.encode(header)
// body
/* istanbul ignore else */
if (value.stream) {
yield * value.stream()
} else {
} /* c8 ignore start */ else {
// shouldn't be here since Blob / File should provide .stream
// and FormData always convert to USVString
/* istanbul ignore next */
yield value
}
} /* c8 ignore stop */
yield linebreak
}
}

View File

@@ -13,7 +13,11 @@ const { isFormDataLike, formDataToStream } = require('./form-data')
const { EventEmitter } = require('node:events')
// request.connectin deprecation https://nodejs.org/api/http.html#http_request_connection
const FST_LIGHTMYREQUEST_DEP01 = createDeprecation({ name: 'FastifyDeprecationLightMyRequest', code: 'FST_LIGHTMYREQUEST_DEP01', message: 'You are accessing "request.connection", use "request.socket" instead.' })
const FST_LIGHTMYREQUEST_DEP01 = createDeprecation({
name: 'FastifyDeprecationLightMyRequest',
code: 'FST_LIGHTMYREQUEST_DEP01',
message: 'You are accessing "request.connection", use "request.socket" instead.'
})
/**
* Get hostname:port
@@ -103,6 +107,7 @@ function Request (options) {
this.headers = {}
this.rawHeaders = []
const headers = options.headers || {}
for (const field in headers) {
@@ -155,6 +160,7 @@ function Request (options) {
payloadResume = true
// we override the content-type
this.headers['content-type'] = stream.contentType
this.headers['transfer-encoding'] = 'chunked'
}
if (payload && typeof payload !== 'string' && !payloadResume && !Buffer.isBuffer(payload)) {
@@ -166,7 +172,7 @@ function Request (options) {
}
// Set the content-length for the corresponding payload if none set
if (payload && !payloadResume && !Object.prototype.hasOwnProperty.call(this.headers, 'content-length')) {
if (payload && !payloadResume && !Object.hasOwn(this.headers, 'content-length')) {
this.headers['content-length'] = (Buffer.isBuffer(payload) ? payload.length : Buffer.byteLength(payload)).toString()
}
@@ -178,50 +184,63 @@ function Request (options) {
this._lightMyRequest = {
payload,
isDone: false,
simulate: options.simulate || {}
simulate: options.simulate || {},
payloadAsStream: options.payloadAsStream,
signal: options.signal
}
const signal = options.signal
/* istanbul ignore if */
/* c8 ignore next 3 */
if (signal) {
addAbortSignal(signal, this)
}
{
const payload = this._lightMyRequest.payload
if (payload?._readableState) { // does quack like a modern stream
this._read = readStream
payload.on('error', (err) => {
this.destroy(err)
})
payload.on('end', () => {
this.push(null)
})
} else {
// Stream v1 are handled in index.js synchronously
this._read = readEverythingElse
}
}
return this
}
util.inherits(Request, Readable)
util.inherits(CustomRequest, Request)
Request.prototype.prepare = function (next) {
function readStream () {
const payload = this._lightMyRequest.payload
if (!payload || typeof payload.resume !== 'function') { // does not quack like a stream
return next()
let more = true
let pushed = false
let chunk
while (more && (chunk = payload.read())) {
pushed = true
more = this.push(chunk)
}
const chunks = []
payload.on('data', (chunk) => chunks.push(Buffer.from(chunk)))
payload.on('end', () => {
const payload = Buffer.concat(chunks)
this.headers['content-length'] = this.headers['content-length'] || ('' + payload.length)
this._lightMyRequest.payload = payload
return next()
})
// Force to resume the stream. Needed for Stream 1
payload.resume()
// We set up a recursive 'readable' event only if we didn't read anything.
// Otheriwse, the stream machinery will call _read() for us.
if (more && !pushed) {
this._lightMyRequest.payload.once('readable', this._read.bind(this))
}
}
Request.prototype._read = function (size) {
function readEverythingElse () {
setImmediate(() => {
if (this._lightMyRequest.isDone) {
// 'end' defaults to true
if (this._lightMyRequest.simulate.end !== false) {
this.push(null)
}
return
}
@@ -251,6 +270,9 @@ Request.prototype._read = function (size) {
})
}
util.inherits(Request, Readable)
util.inherits(CustomRequest, Request)
Request.prototype.destroy = function (error) {
if (this.destroyed || this._lightMyRequest.isDone) return
this.destroyed = true

View File

@@ -1,7 +1,7 @@
'use strict'
const http = require('node:http')
const { Writable, Readable } = require('node:stream')
const { Writable, Readable, addAbortSignal } = require('node:stream')
const util = require('node:util')
const setCookie = require('set-cookie-parser')
@@ -9,7 +9,17 @@ const setCookie = require('set-cookie-parser')
function Response (req, onEnd, reject) {
http.ServerResponse.call(this, req)
this._lightMyRequest = { headers: null, trailers: {}, payloadChunks: [] }
if (req._lightMyRequest?.payloadAsStream) {
const read = this.emit.bind(this, 'drain')
this._lightMyRequest = { headers: null, trailers: {}, stream: new Readable({ read }) }
const signal = req._lightMyRequest.signal
if (signal) {
addAbortSignal(signal, this._lightMyRequest.stream)
}
} else {
this._lightMyRequest = { headers: null, trailers: {}, payloadChunks: [] }
}
// This forces node@8 to always render the headers
this.setHeader('foo', 'bar'); this.removeHeader('foo')
@@ -19,28 +29,51 @@ function Response (req, onEnd, reject) {
let called = false
const onEndSuccess = (payload) => {
// no need to early-return if already called because this handler is bound `once`
if (called) return
called = true
if (this._promiseCallback) {
return process.nextTick(() => onEnd(payload))
}
process.nextTick(() => onEnd(null, payload))
}
this._lightMyRequest.onEndSuccess = onEndSuccess
let finished = false
const onEndFailure = (err) => {
if (called) return
if (called) {
if (this._lightMyRequest.stream && !finished) {
if (!err) {
err = new Error('response destroyed before completion')
err.code = 'LIGHT_ECONNRESET'
}
this._lightMyRequest.stream.destroy(err)
this._lightMyRequest.stream.on('error', () => {})
}
return
}
called = true
if (!err) {
err = new Error('response destroyed before completion')
err.code = 'LIGHT_ECONNRESET'
}
if (this._promiseCallback) {
return process.nextTick(() => reject(err))
}
process.nextTick(() => onEnd(err, null))
}
this.once('finish', () => {
const res = generatePayload(this)
res.raw.req = req
onEndSuccess(res)
})
if (this._lightMyRequest.stream) {
this.once('finish', () => {
finished = true
this._lightMyRequest.stream.push(null)
})
} else {
this.once('finish', () => {
const res = generatePayload(this)
res.raw.req = req
onEndSuccess(res)
})
}
this.connection.once('error', onEndFailure)
@@ -64,6 +97,10 @@ Response.prototype.writeHead = function () {
copyHeaders(this)
if (this._lightMyRequest.stream) {
this._lightMyRequest.onEndSuccess(generatePayload(this))
}
return result
}
@@ -72,8 +109,12 @@ Response.prototype.write = function (data, encoding, callback) {
clearTimeout(this.timeoutHandle)
}
http.ServerResponse.prototype.write.call(this, data, encoding, callback)
this._lightMyRequest.payloadChunks.push(Buffer.from(data, encoding))
return true
if (this._lightMyRequest.stream) {
return this._lightMyRequest.stream.push(Buffer.from(data, encoding))
} else {
this._lightMyRequest.payloadChunks.push(Buffer.from(data, encoding))
return true
}
}
Response.prototype.end = function (data, encoding, callback) {
@@ -110,7 +151,7 @@ Response.prototype.addTrailers = function (trailers) {
function generatePayload (response) {
// This seems only to happen when using `fastify-express` - see https://github.com/fastify/fastify-express/issues/47
/* istanbul ignore if */
/* c8 ignore next 3 */
if (response._lightMyRequest.headers === null) {
copyHeaders(response)
}
@@ -129,22 +170,32 @@ function generatePayload (response) {
}
}
// Prepare payload and trailers
const rawBuffer = Buffer.concat(response._lightMyRequest.payloadChunks)
res.rawPayload = rawBuffer
// we keep both of them for compatibility reasons
res.payload = rawBuffer.toString()
res.body = res.payload
res.trailers = response._lightMyRequest.trailers
// Prepare payload parsers
res.json = function parseJsonPayload () {
return JSON.parse(res.payload)
if (response._lightMyRequest.payloadChunks) {
// Prepare payload and trailers
const rawBuffer = Buffer.concat(response._lightMyRequest.payloadChunks)
res.rawPayload = rawBuffer
// we keep both of them for compatibility reasons
res.payload = rawBuffer.toString()
res.body = res.payload
// Prepare payload parsers
res.json = function parseJsonPayload () {
return JSON.parse(res.payload)
}
} else {
res.json = function () {
throw new Error('Response payload is not available with payloadAsStream: true')
}
}
// Provide stream Readable for advanced user
res.stream = function streamPayload () {
if (response._lightMyRequest.stream) {
return response._lightMyRequest.stream
}
return Readable.from(response._lightMyRequest.payloadChunks)
}
@@ -154,7 +205,7 @@ function generatePayload (response) {
// Throws away all written data to prevent response from buffering payload
function getNullSocket () {
return new Writable({
write (chunk, encoding, callback) {
write (_chunk, _encoding, callback) {
setImmediate(callback)
}
})
@@ -179,7 +230,7 @@ function copyHeaders (response) {
// Add raw headers
;['Date', 'Connection', 'Transfer-Encoding'].forEach((name) => {
const regex = new RegExp('\\r\\n' + name + ': ([^\\r]*)\\r\\n')
const field = response._header.match(regex)
const field = response._header?.match(regex)
if (field) {
response._lightMyRequest.headers[name.toLowerCase()] = field[1]
}

View File

@@ -0,0 +1,2 @@
# Set default behavior to automatically convert line endings
* text=auto eol=lf

View File

@@ -0,0 +1,13 @@
version: 2
updates:
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "monthly"
open-pull-requests-limit: 10
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

View File

@@ -0,0 +1,24 @@
name: CI
on:
push:
branches:
- main
- master
- next
- 'v*'
paths-ignore:
- 'docs/**'
- '*.md'
pull_request:
paths-ignore:
- 'docs/**'
- '*.md'
jobs:
test:
uses: fastify/workflows/.github/workflows/plugins-ci.yml@v5
with:
license-check: true
lint: true
node-versions: '["16", "18", "20", "22"]'

View File

@@ -0,0 +1,2 @@
files:
- test/**/*[!jest].test.js

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) Fastify
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,118 @@
# process-warning
[![CI](https://github.com/fastify/process-warning/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/fastify/process-warning/actions/workflows/ci.yml)
[![NPM version](https://img.shields.io/npm/v/process-warning.svg?style=flat)](https://www.npmjs.com/package/process-warning)
[![neostandard javascript style](https://img.shields.io/badge/code_style-neostandard-brightgreen?style=flat)](https://github.com/neostandard/neostandard)
A small utility for generating consistent warning objects across your codebase.
It also exposes a utility for emitting those warnings, guaranteeing that they are issued only once (unless configured otherwise).
_This module is used by the [Fastify](https://fastify.dev) framework and it was called `fastify-warning` prior to version 1.0.0._
### Install
```
npm i process-warning
```
### Usage
The module exports two builder functions for creating warnings.
```js
const {
createWarning,
createDeprecation
} = require('process-warning')
const warning = createWarning({
name: 'ExampleWarning',
code: 'EXP_WRN_001',
message: 'Hello %s',
unlimited: true
})
warning('world')
```
#### Methods
##### `createWarning({ name, code, message[, unlimited] })`
- `name` (`string`, required) - The error name, you can access it later with
`error.name`. For consistency, we recommend prefixing module error names
with `{YourModule}Warning`
- `code` (`string`, required) - The warning code, you can access it later with
`error.code`. For consistency, we recommend prefixing plugin error codes with
`{ThreeLetterModuleName}_`, e.g. `FST_`. NOTE: codes should be all uppercase.
- `message` (`string`, required) - The warning message. You can also use
interpolated strings for formatting the message.
- `options` (`object`, optional) - Optional options with the following
properties:
+ `unlimited` (`boolean`, optional) - Should the warning be emitted more than
once? Defaults to `false`.
##### `createDeprecation({code, message[, options]})`
This is a wrapper for `createWarning`. It is equivalent to invoking
`createWarning` with the `name` parameter set to "DeprecationWarning".
Deprecation warnings have extended support for the Node.js CLI options:
`--throw-deprecation`, `--no-deprecation`, and `--trace-deprecation`.
##### `warning([, a [, b [, c]]])`
The returned `warning` function can used for emitting warnings.
A warning is guaranteed to be emitted at least once.
- `[, a [, b [, c]]]` (`any`, optional) - Parameters for string interpolation.
```js
const { createWarning } = require('process-warning')
const FST_ERROR_CODE = createWarning({ name: 'MyAppWarning', code: 'FST_ERROR_CODE', message: 'message' })
FST_ERROR_CODE()
```
How to use an interpolated string:
```js
const { createWarning } = require('process-warning')
const FST_ERROR_CODE = createWarning({ name: 'MyAppWarning', code: 'FST_ERROR_CODE', message: 'Hello %s'})
FST_ERROR_CODE('world')
```
The `warning` object has methods and properties for managing the warning's state. Useful for testing.
```js
const { createWarning } = require('process-warning')
const FST_ERROR_CODE = createWarning({ name: 'MyAppWarning', code: 'FST_ERROR_CODE', message: 'Hello %s'})
console.log(FST_ERROR_CODE.emitted) // false
FST_ERROR_CODE('world')
console.log(FST_ERROR_CODE.emitted) // true
const FST_ERROR_CODE_2 = createWarning('MyAppWarning', 'FST_ERROR_CODE_2', 'Hello %s')
FST_ERROR_CODE_2.emitted = true
FST_ERROR_CODE_2('world') // will not be emitted because it is not unlimited
```
How to use an unlimited warning:
```js
const { createWarning } = require('process-warning')
const FST_ERROR_CODE = createWarning({ name: 'MyAppWarning', code: 'FST_ERROR_CODE', message: 'Hello %s', unlimited: true })
FST_ERROR_CODE('world') // will be emitted
FST_ERROR_CODE('world') // will be emitted again
```
#### Suppressing warnings
It is possible to suppress warnings by utilizing one of node's built-in warning suppression mechanisms.
Warnings can be suppressed:
- by setting the `NODE_NO_WARNINGS` environment variable to `1`
- by passing the `--no-warnings` flag to the node process
- by setting '--no-warnings' in the `NODE_OPTIONS` environment variable
For more information see [node's documentation](https://nodejs.org/api/cli.html).
## License
Licensed under [MIT](./LICENSE).

View File

@@ -0,0 +1,25 @@
'use strict'
const { Suite } = require('benchmark')
const { createWarning } = require('..')
const err1 = createWarning({
name: 'TestWarning',
code: 'TST_ERROR_CODE_1',
message: 'message'
})
const err2 = createWarning({
name: 'TestWarning',
code: 'TST_ERROR_CODE_2',
message: 'message'
})
new Suite()
.add('warn', function () {
err1()
err2()
})
.on('cycle', function (event) {
console.log(String(event.target))
})
.run()

View File

@@ -0,0 +1,6 @@
'use strict'
module.exports = require('neostandard')({
ignores: require('neostandard').resolveIgnoresFromGitignore(),
ts: true
})

View File

@@ -0,0 +1,11 @@
'use strict'
const { createWarning } = require('..')
const CUSTDEP001 = createWarning({
name: 'DeprecationWarning',
code: 'CUSTDEP001',
message: 'This is a deprecation warning'
})
CUSTDEP001()

View File

@@ -0,0 +1,124 @@
'use strict'
const { format } = require('node:util')
/**
* @namespace processWarning
*/
/**
* Represents a warning item with details.
* @typedef {Function} WarningItem
* @param {*} [a] Possible message interpolation value.
* @param {*} [b] Possible message interpolation value.
* @param {*} [c] Possible message interpolation value.
* @property {string} name - The name of the warning.
* @property {string} code - The code associated with the warning.
* @property {string} message - The warning message.
* @property {boolean} emitted - Indicates if the warning has been emitted.
* @property {function} format - Formats the warning message.
*/
/**
* Options for creating a process warning.
* @typedef {Object} ProcessWarningOptions
* @property {string} name - The name of the warning.
* @property {string} code - The code associated with the warning.
* @property {string} message - The warning message.
* @property {boolean} [unlimited=false] - If true, allows unlimited emissions of the warning.
*/
/**
* Represents the process warning functionality.
* @typedef {Object} ProcessWarning
* @property {function} createWarning - Creates a warning item.
* @property {function} createDeprecation - Creates a deprecation warning item.
*/
/**
* Creates a deprecation warning item.
* @function
* @memberof processWarning
* @param {ProcessWarningOptions} params - Options for creating the warning.
* @returns {WarningItem} The created deprecation warning item.
*/
function createDeprecation (params) {
return createWarning({ ...params, name: 'DeprecationWarning' })
}
/**
* Creates a warning item.
* @function
* @memberof processWarning
* @param {ProcessWarningOptions} params - Options for creating the warning.
* @returns {WarningItem} The created warning item.
* @throws {Error} Throws an error if name, code, or message is empty, or if opts.unlimited is not a boolean.
*/
function createWarning ({ name, code, message, unlimited = false } = {}) {
if (!name) throw new Error('Warning name must not be empty')
if (!code) throw new Error('Warning code must not be empty')
if (!message) throw new Error('Warning message must not be empty')
if (typeof unlimited !== 'boolean') throw new Error('Warning opts.unlimited must be a boolean')
code = code.toUpperCase()
let warningContainer = {
[name]: function (a, b, c) {
if (warning.emitted === true && warning.unlimited !== true) {
return
}
warning.emitted = true
process.emitWarning(warning.format(a, b, c), warning.name, warning.code)
}
}
if (unlimited) {
warningContainer = {
[name]: function (a, b, c) {
warning.emitted = true
process.emitWarning(warning.format(a, b, c), warning.name, warning.code)
}
}
}
const warning = warningContainer[name]
warning.emitted = false
warning.message = message
warning.unlimited = unlimited
warning.code = code
/**
* Formats the warning message.
* @param {*} [a] Possible message interpolation value.
* @param {*} [b] Possible message interpolation value.
* @param {*} [c] Possible message interpolation value.
* @returns {string} The formatted warning message.
*/
warning.format = function (a, b, c) {
let formatted
if (a && b && c) {
formatted = format(message, a, b, c)
} else if (a && b) {
formatted = format(message, a, b)
} else if (a) {
formatted = format(message, a)
} else {
formatted = message
}
return formatted
}
return warning
}
/**
* Module exports containing the process warning functionality.
* @namespace
* @property {function} createWarning - Creates a warning item.
* @property {function} createDeprecation - Creates a deprecation warning item.
* @property {ProcessWarning} processWarning - Represents the process warning functionality.
*/
const out = { createWarning, createDeprecation }
module.exports = out
module.exports.default = out
module.exports.processWarning = out

View File

@@ -0,0 +1,73 @@
{
"name": "process-warning",
"version": "4.0.1",
"description": "A small utility for creating warnings and emitting them.",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"scripts": {
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run test:unit && npm run test:jest && npm run test:typescript",
"test:jest": "jest jest.test.js",
"test:unit": "tap",
"test:typescript": "tsd"
},
"repository": {
"type": "git",
"url": "git+https://github.com/fastify/process-warning.git"
},
"keywords": [
"fastify",
"error",
"warning",
"utility",
"plugin",
"emit",
"once"
],
"author": "Tomas Della Vedova",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Manuel Spigolon",
"email": "behemoth89@gmail.com"
},
{
"name": "James Sumners",
"url": "https://james.sumners.info"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "MIT",
"bugs": {
"url": "https://github.com/fastify/fastify-warning/issues"
},
"homepage": "https://github.com/fastify/fastify-warning#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
],
"devDependencies": {
"@fastify/pre-commit": "^2.1.0",
"benchmark": "^2.1.4",
"eslint": "^9.17.0",
"jest": "^29.7.0",
"neostandard": "^0.12.0",
"tap": "^18.7.2",
"tsd": "^0.31.0"
}
}

View File

@@ -0,0 +1,29 @@
'use strict'
const test = require('tap').test
const { createWarning } = require('..')
test('emit with interpolated string', t => {
t.plan(4)
process.on('warning', onWarning)
function onWarning (warning) {
t.equal(warning.name, 'TestDeprecation')
t.equal(warning.code, 'CODE')
t.equal(warning.message, 'Hello world')
t.ok(codeWarning.emitted)
}
const codeWarning = createWarning({
name: 'TestDeprecation',
code: 'CODE',
message: 'Hello %s'
})
codeWarning('world')
codeWarning('world')
setImmediate(() => {
process.removeListener('warning', onWarning)
t.end()
})
})

View File

@@ -0,0 +1,28 @@
'use strict'
const test = require('tap').test
const { createWarning } = require('..')
test('emit should emit a given code only once', t => {
t.plan(4)
process.on('warning', onWarning)
function onWarning (warning) {
t.equal(warning.name, 'TestDeprecation')
t.equal(warning.code, 'CODE')
t.equal(warning.message, 'Hello world')
t.ok(warn.emitted)
}
const warn = createWarning({
name: 'TestDeprecation',
code: 'CODE',
message: 'Hello world'
})
warn()
warn()
setImmediate(() => {
process.removeListener('warning', onWarning)
t.end()
})
})

View File

@@ -0,0 +1,36 @@
'use strict'
const test = require('tap').test
const { createWarning } = require('../')
test('a limited warning can be re-set', t => {
t.plan(4)
let count = 0
process.on('warning', onWarning)
function onWarning () {
count++
}
const warn = createWarning({
name: 'TestDeprecation',
code: 'CODE',
message: 'Hello world'
})
warn()
t.ok(warn.emitted)
warn()
t.ok(warn.emitted)
warn.emitted = false
warn()
t.ok(warn.emitted)
setImmediate(() => {
t.equal(count, 2)
process.removeListener('warning', onWarning)
t.end()
})
})

View File

@@ -0,0 +1,30 @@
'use strict'
const test = require('tap').test
const { createWarning } = require('../')
test('emit should set the emitted state', t => {
t.plan(3)
process.on('warning', onWarning)
function onWarning () {
t.fail('should not be called')
}
const warn = createWarning({
name: 'TestDeprecation',
code: 'CODE',
message: 'Hello world'
})
t.notOk(warn.emitted)
warn.emitted = true
t.ok(warn.emitted)
warn()
t.ok(warn.emitted)
setImmediate(() => {
process.removeListener('warning', onWarning)
t.end()
})
})

View File

@@ -0,0 +1,37 @@
'use strict'
const test = require('tap').test
const { createWarning } = require('..')
test('emit should emit a given code unlimited times', t => {
t.plan(50)
let runs = 0
const expectedRun = []
const times = 10
process.on('warning', onWarning)
function onWarning (warning) {
t.equal(warning.name, 'TestDeprecation')
t.equal(warning.code, 'CODE')
t.equal(warning.message, 'Hello world')
t.ok(warn.emitted)
t.equal(runs++, expectedRun.shift())
}
const warn = createWarning({
name: 'TestDeprecation',
code: 'CODE',
message: 'Hello world',
unlimited: true
})
for (let i = 0; i < times; i++) {
expectedRun.push(i)
warn()
}
setImmediate(() => {
process.removeListener('warning', onWarning)
t.end()
})
})

View File

@@ -0,0 +1,99 @@
'use strict'
const test = require('tap').test
const { createWarning, createDeprecation } = require('..')
process.removeAllListeners('warning')
test('Create warning with zero parameter', t => {
t.plan(3)
const warnItem = createWarning({
name: 'TestWarning',
code: 'CODE',
message: 'Not available'
})
t.equal(warnItem.name, 'TestWarning')
t.equal(warnItem.message, 'Not available')
t.equal(warnItem.code, 'CODE')
})
test('Create error with 1 parameter', t => {
t.plan(3)
const warnItem = createWarning({
name: 'TestWarning',
code: 'CODE',
message: 'hey %s'
})
t.equal(warnItem.name, 'TestWarning')
t.equal(warnItem.format('alice'), 'hey alice')
t.equal(warnItem.code, 'CODE')
})
test('Create error with 2 parameters', t => {
t.plan(3)
const warnItem = createWarning({
name: 'TestWarning',
code: 'CODE',
message: 'hey %s, I like your %s'
})
t.equal(warnItem.name, 'TestWarning')
t.equal(warnItem.format('alice', 'attitude'), 'hey alice, I like your attitude')
t.equal(warnItem.code, 'CODE')
})
test('Create error with 3 parameters', t => {
t.plan(3)
const warnItem = createWarning({
name: 'TestWarning',
code: 'CODE',
message: 'hey %s, I like your %s %s'
})
t.equal(warnItem.name, 'TestWarning')
t.equal(warnItem.format('alice', 'attitude', 'see you'), 'hey alice, I like your attitude see you')
t.equal(warnItem.code, 'CODE')
})
test('Creates a deprecation warning', t => {
t.plan(3)
const deprecationItem = createDeprecation({
name: 'DeprecationWarning',
code: 'CODE',
message: 'hello %s'
})
t.equal(deprecationItem.name, 'DeprecationWarning')
t.equal(deprecationItem.format('world'), 'hello world')
t.equal(deprecationItem.code, 'CODE')
})
test('Should throw when error code has no name', t => {
t.plan(1)
t.throws(() => createWarning(), new Error('Warning name must not be empty'))
})
test('Should throw when error has no code', t => {
t.plan(1)
t.throws(() => createWarning({ name: 'name' }), new Error('Warning code must not be empty'))
})
test('Should throw when error has no message', t => {
t.plan(1)
t.throws(() => createWarning({
name: 'name',
code: 'code'
}), new Error('Warning message must not be empty'))
})
test('Cannot set unlimited other than boolean', t => {
t.plan(1)
t.throws(() => createWarning({
name: 'name',
code: 'code',
message: 'message',
unlimited: 'unlimited'
}), new Error('Warning opts.unlimited must be a boolean'))
})

View File

@@ -0,0 +1,33 @@
'use strict'
const { test } = require('tap')
const { createWarning } = require('..')
test('Must not overwrite config', t => {
t.plan(1)
function onWarning (warning) {
t.equal(warning.code, 'CODE_1')
}
const a = createWarning({
name: 'TestWarning',
code: 'CODE_1',
message: 'Msg'
})
createWarning({
name: 'TestWarning',
code: 'CODE_2',
message: 'Msg',
unlimited: true
})
process.on('warning', onWarning)
a('CODE_1')
a('CODE_1')
setImmediate(() => {
process.removeListener('warning', onWarning)
t.end()
})
})

View File

@@ -0,0 +1,22 @@
/* global test, expect */
'use strict'
const { createWarning } = require('..')
test('works with jest', done => {
const code = createWarning({
name: 'TestDeprecation',
code: 'CODE',
message: 'Hello world'
})
code('world')
// we cannot actually listen to process warning event
// because jest messes with it (that's the point of this test)
// we can only test it was emitted indirectly
// and test no exception is raised
setImmediate(() => {
expect(code.emitted).toBeTruthy()
done()
})
})

View File

@@ -0,0 +1,80 @@
'use strict'
const { test } = require('tap')
const { spawnSync } = require('node:child_process')
const { resolve } = require('node:path')
const entry = resolve(__dirname, '../examples', 'example.js')
test('--no-warnings is set in cli', t => {
t.plan(1)
const child = spawnSync(process.execPath, [
'--no-warnings',
entry
])
const stderr = child.stderr.toString()
t.equal(stderr, '')
})
test('--no-warnings is not set in cli', t => {
t.plan(1)
const child = spawnSync(process.execPath, [
entry
])
const stderr = child.stderr.toString()
t.match(stderr, /\[CUSTDEP001\] DeprecationWarning: This is a deprecation warning/)
})
test('NODE_NO_WARNINGS is set to 1', t => {
t.plan(1)
const child = spawnSync(process.execPath, [
entry
], {
env: {
NODE_NO_WARNINGS: '1'
}
})
const stderr = child.stderr.toString()
t.equal(stderr, '')
})
test('NODE_NO_WARNINGS is set to 0', t => {
t.plan(1)
const child = spawnSync(process.execPath, [
entry
], {
env: {
NODE_NO_WARNINGS: '0'
}
})
const stderr = child.stderr.toString()
t.match(stderr, /\[CUSTDEP001\] DeprecationWarning: This is a deprecation warning/)
})
test('NODE_NO_WARNINGS is not set', t => {
t.plan(1)
const child = spawnSync(process.execPath, [
entry
])
const stderr = child.stderr.toString()
t.match(stderr, /\[CUSTDEP001\] DeprecationWarning: This is a deprecation warning/)
})
test('NODE_Options contains --no-warnings', t => {
t.plan(1)
const child = spawnSync(process.execPath, [
entry
], {
env: {
NODE_OPTIONS: '--no-warnings'
}
})
const stderr = child.stderr.toString()
t.equal(stderr, '')
})

View File

@@ -0,0 +1,37 @@
declare namespace processWarning {
export interface WarningItem {
(a?: any, b?: any, c?: any): void;
name: string;
code: string;
message: string;
emitted: boolean;
unlimited: boolean;
format(a?: any, b?: any, c?: any): string;
}
export type WarningOptions = {
name: string;
code: string;
message: string;
unlimited?: boolean;
}
export type DeprecationOptions = Omit<WarningOptions, 'name'>
export type ProcessWarningOptions = {
unlimited?: boolean;
}
export type ProcessWarning = {
createWarning(params: WarningOptions): WarningItem;
createDeprecation(params: DeprecationOptions): WarningItem;
}
export function createWarning (params: WarningOptions): WarningItem
export function createDeprecation (params: DeprecationOptions): WarningItem
const processWarning: ProcessWarning
export { processWarning as default }
}
export = processWarning

View File

@@ -0,0 +1,36 @@
import { expectType } from 'tsd'
import { createWarning, createDeprecation } from '..'
const WarnInstance = createWarning({
name: 'TypeScriptWarning',
code: 'CODE',
message: 'message'
})
expectType<string>(WarnInstance.code)
expectType<string>(WarnInstance.message)
expectType<string>(WarnInstance.name)
expectType<boolean>(WarnInstance.emitted)
expectType<boolean>(WarnInstance.unlimited)
expectType<void>(WarnInstance())
expectType<void>(WarnInstance('foo'))
expectType<void>(WarnInstance('foo', 'bar'))
const buildWarnUnlimited = createWarning({
name: 'TypeScriptWarning',
code: 'CODE',
message: 'message',
unlimited: true
})
expectType<boolean>(buildWarnUnlimited.unlimited)
const DeprecationInstance = createDeprecation({
code: 'CODE',
message: 'message'
})
expectType<string>(DeprecationInstance.code)
DeprecationInstance()
DeprecationInstance('foo')
DeprecationInstance('foo', 'bar')

View File

@@ -1,37 +1,40 @@
{
"name": "light-my-request",
"version": "5.14.0",
"version": "6.6.0",
"description": "Fake HTTP injection library",
"main": "index.js",
"type": "commonjs",
"types": "types/index.d.ts",
"dependencies": {
"cookie": "^0.7.0",
"process-warning": "^3.0.0",
"set-cookie-parser": "^2.4.1"
"cookie": "^1.0.1",
"process-warning": "^4.0.0",
"set-cookie-parser": "^2.6.0"
},
"devDependencies": {
"@fastify/ajv-compiler": "^3.1.0",
"@fastify/pre-commit": "^2.0.2",
"@types/node": "^20.1.0",
"@fastify/ajv-compiler": "^4.0.0",
"@fastify/pre-commit": "^2.1.0",
"@types/node": "^22.7.7",
"c8": "^10.1.2",
"end-of-stream": "^1.4.4",
"express": "^4.17.1",
"form-auto-content": "^3.0.0",
"eslint": "^9.17.0",
"express": "^4.19.2",
"form-auto-content": "^3.2.1",
"form-data": "^4.0.0",
"formdata-node": "^4.4.1",
"standard": "^17.0.0",
"tap": "^16.0.0",
"tinybench": "^2.5.1",
"formdata-node": "^6.0.3",
"multer": "^1.4.5-lts.1",
"neostandard": "^0.12.0",
"tinybench": "^3.0.0",
"tsd": "^0.31.0",
"undici": "^5.28.4"
"undici": "^7.0.0"
},
"scripts": {
"benchmark": "node benchmark/benchmark.js",
"coverage": "npm run unit -- --cov --coverage-report=html",
"lint": "standard",
"lint": "eslint",
"lint:fix": "eslint --fix",
"test": "npm run lint && npm run test:unit && npm run test:typescript",
"test:typescript": "tsd",
"test:unit": "tap"
"test:unit": "c8 --100 node --test"
},
"repository": {
"type": "git",
@@ -45,14 +48,38 @@
"server"
],
"author": "Tomas Della Vedova - @delvedor (http://delved.org)",
"contributors": [
{
"name": "Matteo Collina",
"email": "hello@matteocollina.com"
},
{
"name": "Manuel Spigolon",
"email": "behemoth89@gmail.com"
},
{
"name": "Aras Abbasi",
"email": "aras.abbasi@gmail.com"
},
{
"name": "Frazer Smith",
"email": "frazer.dev@icloud.com",
"url": "https://github.com/fdawgs"
}
],
"license": "BSD-3-Clause",
"bugs": {
"url": "https://github.com/fastify/light-my-request/issues"
},
"homepage": "https://github.com/fastify/light-my-request/blob/master/README.md",
"standard": {
"ignore": [
"test/benchmark.js"
]
}
"homepage": "https://github.com/fastify/light-my-request#readme",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/fastify"
},
{
"type": "opencollective",
"url": "https://opencollective.com/fastify"
}
]
}

View File

@@ -1,32 +1,32 @@
'use strict'
const { test } = require('tap')
const { test } = require('node:test')
const inject = require('../index')
test('basic async await', async t => {
const dispatch = function (req, res) {
const dispatch = function (_req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('hello')
}
try {
const res = await inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello' })
t.equal(res.payload, 'hello')
t.assert.strictEqual(res.payload, 'hello')
} catch (err) {
t.fail(err)
t.assert.fail(err)
}
})
test('basic async await (errored)', async t => {
const dispatch = function (req, res) {
const dispatch = function (_req, res) {
res.connection.destroy(new Error('kaboom'))
}
await t.rejects(inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello' }))
await t.assert.rejects(() => inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello' }), Error)
})
test('chainable api with async await', async t => {
const dispatch = function (req, res) {
const dispatch = function (_req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('hello')
}
@@ -34,22 +34,22 @@ test('chainable api with async await', async t => {
try {
const chain = inject(dispatch).get('http://example.com:8080/hello')
const res = await chain.end()
t.equal(res.payload, 'hello')
t.assert.strictEqual(res.payload, 'hello')
} catch (err) {
t.fail(err)
t.assert.fail(err)
}
})
test('chainable api with async await without end()', async t => {
const dispatch = function (req, res) {
const dispatch = function (_req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('hello')
}
try {
const res = await inject(dispatch).get('http://example.com:8080/hello')
t.equal(res.payload, 'hello')
t.assert.strictEqual(res.payload, 'hello')
} catch (err) {
t.fail(err)
t.assert.fail(err)
}
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
'use strict'
const { test } = require('tap')
const { test } = require('node:test')
const Request = require('../lib/request')
@@ -12,5 +12,5 @@ test('aborted property should be false', async (t) => {
}
const req = new Request(mockReq)
t.same(req.aborted, false)
t.assert.strictEqual(req.aborted, false)
})

View File

@@ -1,15 +1,17 @@
'use strict'
const { test } = require('tap')
const { test } = require('node:test')
const Response = require('../lib/response')
test('multiple calls to res.destroy should not be called', (t) => {
t.plan(1)
test('multiple calls to res.destroy should not be called', (t, done) => {
t.plan(2)
const mockReq = {}
const res = new Response(mockReq, (err, response) => {
t.error(err)
const res = new Response(mockReq, (err) => {
t.assert.ok(err)
t.assert.strictEqual(err.code, 'LIGHT_ECONNRESET')
done()
})
res.destroy()

View File

@@ -0,0 +1,359 @@
'use strict'
const t = require('node:test')
const fs = require('node:fs')
const test = t.test
const zlib = require('node:zlib')
const express = require('express')
const inject = require('../index')
function accumulate (stream, cb) {
const chunks = []
stream.on('error', cb)
stream.on('data', (chunk) => {
chunks.push(chunk)
})
stream.on('end', () => {
cb(null, Buffer.concat(chunks))
})
}
test('stream mode - non-chunked payload', (t, done) => {
t.plan(9)
const output = 'example.com:8080|/hello'
const dispatch = function (req, res) {
res.statusMessage = 'Super'
res.setHeader('x-extra', 'hello')
res.writeHead(200, { 'Content-Type': 'text/plain', 'Content-Length': output.length })
res.end(req.headers.host + '|' + req.url)
}
inject(dispatch, {
url: 'http://example.com:8080/hello',
payloadAsStream: true
}, (err, res) => {
t.assert.ifError(err)
t.assert.strictEqual(res.statusCode, 200)
t.assert.strictEqual(res.statusMessage, 'Super')
t.assert.ok(res.headers.date)
t.assert.deepStrictEqual(res.headers, {
date: res.headers.date,
connection: 'keep-alive',
'x-extra': 'hello',
'content-type': 'text/plain',
'content-length': output.length.toString()
})
t.assert.strictEqual(res.payload, undefined)
t.assert.strictEqual(res.rawPayload, undefined)
accumulate(res.stream(), (err, payload) => {
t.assert.ifError(err)
t.assert.strictEqual(payload.toString(), 'example.com:8080|/hello')
done()
})
})
})
test('stream mode - passes headers', (t, done) => {
t.plan(3)
const dispatch = function (req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end(req.headers.super)
}
inject(dispatch, {
method: 'GET',
url: 'http://example.com:8080/hello',
headers: { Super: 'duper' },
payloadAsStream: true
}, (err, res) => {
t.assert.ifError(err)
accumulate(res.stream(), (err, payload) => {
t.assert.ifError(err)
t.assert.strictEqual(payload.toString(), 'duper')
done()
})
})
})
test('stream mode - returns chunked payload', (t, done) => {
t.plan(6)
const dispatch = function (_req, res) {
res.writeHead(200, 'OK')
res.write('a')
res.write('b')
res.end()
}
inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {
t.assert.ifError(err)
t.assert.ok(res.headers.date)
t.assert.ok(res.headers.connection)
t.assert.strictEqual(res.headers['transfer-encoding'], 'chunked')
accumulate(res.stream(), (err, payload) => {
t.assert.ifError(err)
t.assert.strictEqual(payload.toString(), 'ab')
done()
})
})
})
test('stream mode - backpressure', (t, done) => {
t.plan(7)
let expected
const dispatch = function (_req, res) {
res.writeHead(200, 'OK')
res.write('a')
const buf = Buffer.alloc(1024 * 1024).fill('b')
t.assert.strictEqual(res.write(buf), false)
expected = 'a' + buf.toString()
res.on('drain', () => {
res.end()
})
}
inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {
t.assert.ifError(err)
t.assert.ok(res.headers.date)
t.assert.ok(res.headers.connection)
t.assert.strictEqual(res.headers['transfer-encoding'], 'chunked')
accumulate(res.stream(), (err, payload) => {
t.assert.ifError(err)
t.assert.strictEqual(payload.toString(), expected)
done()
})
})
})
test('stream mode - sets trailers in response object', (t, done) => {
t.plan(4)
const dispatch = function (_req, res) {
res.setHeader('Trailer', 'Test')
res.addTrailers({ Test: 123 })
res.end()
}
inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {
t.assert.ifError(err)
t.assert.strictEqual(res.headers.trailer, 'Test')
t.assert.strictEqual(res.headers.test, undefined)
t.assert.strictEqual(res.trailers.test, '123')
done()
})
})
test('stream mode - parses zipped payload', (t, done) => {
t.plan(5)
const dispatch = function (_req, res) {
res.writeHead(200, 'OK')
const stream = fs.createReadStream('./package.json')
stream.pipe(zlib.createGzip()).pipe(res)
}
inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {
t.assert.ifError(err)
fs.readFile('./package.json', { encoding: 'utf-8' }, (err, file) => {
t.assert.ifError(err)
accumulate(res.stream(), (err, payload) => {
t.assert.ifError(err)
zlib.unzip(payload, (err, unzipped) => {
t.assert.ifError(err)
t.assert.strictEqual(unzipped.toString('utf-8'), file)
done()
})
})
})
})
})
test('stream mode - returns multi buffer payload', (t, done) => {
t.plan(3)
const dispatch = function (_req, res) {
res.writeHead(200)
res.write('a')
res.write(Buffer.from('b'))
res.end()
}
inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {
t.assert.ifError(err)
const chunks = []
const stream = res.stream()
stream.on('data', (chunk) => {
chunks.push(chunk)
})
stream.on('end', () => {
t.assert.strictEqual(chunks.length, 2)
t.assert.strictEqual(Buffer.concat(chunks).toString(), 'ab')
done()
})
})
})
test('stream mode - returns null payload', (t, done) => {
t.plan(4)
const dispatch = function (_req, res) {
res.writeHead(200, { 'Content-Length': 0 })
res.end()
}
inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {
t.assert.ifError(err)
t.assert.strictEqual(res.payload, undefined)
accumulate(res.stream(), (err, payload) => {
t.assert.ifError(err)
t.assert.strictEqual(payload.toString(), '')
done()
})
})
})
test('stream mode - simulates error', (t, done) => {
t.plan(3)
const dispatch = function (req, res) {
req.on('readable', () => {
})
req.on('error', () => {
res.writeHead(200, { 'Content-Length': 0 })
res.end('error')
})
}
const body = 'something special just for you'
inject(dispatch, { method: 'GET', url: '/', payload: body, simulate: { error: true }, payloadAsStream: true }, (err, res) => {
t.assert.ifError(err)
accumulate(res.stream(), (err, payload) => {
t.assert.ifError(err)
t.assert.strictEqual(payload.toString(), 'error')
done()
})
})
})
test('stream mode - promises support', (t, done) => {
t.plan(1)
const dispatch = function (_req, res) {
res.writeHead(200, { 'Content-Type': 'text/plain' })
res.end('hello')
}
inject(dispatch, { method: 'GET', url: 'http://example.com:8080/hello', payloadAsStream: true })
.then((res) => {
return new Promise((resolve, reject) => {
accumulate(res.stream(), (err, payload) => {
if (err) {
return reject(err)
}
resolve(payload)
})
})
})
.then(payload => t.assert.strictEqual(payload.toString(), 'hello'))
.catch(t.assert.fail)
.finally(done)
})
test('stream mode - Response.json() should throw', (t, done) => {
t.plan(2)
const jsonData = {
a: 1,
b: '2'
}
const dispatch = function (_req, res) {
res.writeHead(200, { 'Content-Type': 'application/json' })
res.end(JSON.stringify(jsonData))
}
inject(dispatch, { method: 'GET', path: 'http://example.com:8080/hello', payloadAsStream: true }, (err, res) => {
t.assert.ifError(err)
const { json } = res
t.assert.throws(json, Error)
done()
})
})
test('stream mode - error for response destroy', (t, done) => {
t.plan(2)
const dispatch = function (_req, res) {
res.writeHead(200)
setImmediate(() => {
res.destroy()
})
}
inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {
t.assert.ifError(err)
accumulate(res.stream(), (err) => {
t.assert.ok(err)
done()
})
})
})
test('stream mode - request destroy with error', (t, done) => {
t.plan(3)
const fakeError = new Error('some-err')
const dispatch = function (req) {
req.destroy(fakeError)
}
inject(dispatch, { method: 'GET', url: '/', payloadAsStream: true }, (err, res) => {
t.assert.ok(err)
t.assert.strictEqual(err, fakeError)
t.assert.strictEqual(res, null)
done()
})
})
test('stream mode - Can abort a request using AbortController/AbortSignal', async (t) => {
const dispatch = function (_req, res) {
res.writeHead(200)
}
const controller = new AbortController()
const res = await inject(dispatch, {
method: 'GET',
url: 'http://example.com:8080/hello',
signal: controller.signal,
payloadAsStream: true
})
controller.abort()
await t.assert.rejects(async () => {
for await (const c of res.stream()) {
t.assert.fail(`should not loop, got ${c.toString()}`)
}
}, Error)
}, { skip: globalThis.AbortController == null })
test("stream mode - passes payload when using express' send", (t, done) => {
t.plan(4)
const app = express()
app.get('/hello', (_req, res) => {
res.send('some text')
})
inject(app, { method: 'GET', url: 'http://example.com:8080/hello', payloadAsStream: true }, (err, res) => {
t.assert.ifError(err)
t.assert.strictEqual(res.headers['content-length'], '9')
accumulate(res.stream(), function (err, payload) {
t.assert.ifError(err)
t.assert.strictEqual(payload.toString(), 'some text')
done()
})
})
})

View File

@@ -1,5 +1,5 @@
import * as http from 'http'
import { Readable } from 'stream'
import * as http from 'node:http'
import { Readable } from 'node:stream'
type HTTPMethods = 'DELETE' | 'delete' |
'GET' | 'get' |
@@ -59,6 +59,7 @@ declare namespace inject {
cookies?: { [k: string]: string },
signal?: AbortSignal,
Request?: object,
payloadAsStream?: boolean
}
/**
@@ -104,7 +105,7 @@ declare namespace inject {
body: (body: InjectPayload) => Chain
headers: (headers: http.IncomingHttpHeaders | http.OutgoingHttpHeaders) => Chain
payload: (payload: InjectPayload) => Chain
query: (query: string | { [k: string]: string | string[] } ) => Chain
query: (query: string | { [k: string]: string | string[] }) => Chain
cookies: (query: object) => Chain
end(): Promise<Response>
end(callback: CallbackFunc): void

View File

@@ -1,7 +1,7 @@
import * as http from 'http'
import * as http from 'node:http'
import { inject, isInjection, Response, DispatchFunc, InjectOptions, Chain } from '..'
import { expectType, expectAssignable, expectNotAssignable } from 'tsd'
import { Readable } from 'stream'
import { Readable } from 'node:stream'
expectAssignable<InjectOptions>({ url: '/' })
expectAssignable<InjectOptions>({ autoStart: true })
@@ -22,7 +22,7 @@ const dispatch: http.RequestListener = function (req, res) {
const expectResponse = function (res: Response | undefined) {
if (!res) {
return;
return
}
expectType<Response>(res)
console.log(res.payload)
@@ -142,3 +142,8 @@ inject(httpDispatch, { method: 'get', url: '/' }, (err, res) => {
expectType<Error | undefined>(err)
expectResponse(res)
})
inject(httpDispatch, { method: 'get', url: '/', payloadAsStream: true }, (err, res) => {
expectType<Error | undefined>(err)
expectResponse(res)
})