Comparing version 4.4.1 to 4.4.2
@@ -18,2 +18,3 @@ 'use strict' | ||
const nodeMajor = Number(process.versions.node.split('.')[0]) | ||
const nodeMinor = Number(process.versions.node.split('.')[1]) | ||
@@ -90,3 +91,3 @@ Object.assign(Dispatcher.prototype, api) | ||
if (nodeMajor >= 16) { | ||
if (nodeMajor > 16 || (nodeMajor === 16 && nodeMinor >= 5)) { | ||
const fetchImpl = require('./lib/fetch') | ||
@@ -93,0 +94,0 @@ module.exports.fetch = async function fetch (resource, init) { |
@@ -9,2 +9,3 @@ // Ported from https://github.com/nodejs/undici/pull/907 | ||
const util = require('../core/util') | ||
const { toWebReadable } = require('../fetch/util') | ||
@@ -134,3 +135,3 @@ let Blob | ||
if (!this[kBody]) { | ||
this[kBody] = util.toWeb(this) | ||
this[kBody] = toWebReadable(this) | ||
if (this[kConsume]) { | ||
@@ -137,0 +138,0 @@ // TODO: Is this the best way to force a lock? |
@@ -266,10 +266,4 @@ 'use strict' | ||
function isBodyReadable (body) { | ||
// This is a hack! | ||
return body && /state: 'readable'/.test(nodeUtil.inspect(body.stream)) | ||
} | ||
module.exports = { | ||
extractBody, | ||
isBodyReadable, | ||
safelyExtractBody, | ||
@@ -276,0 +270,0 @@ cloneBody, |
@@ -13,6 +13,9 @@ // https://github.com/Ethan-Arrowood/undici-fetch | ||
const { Request, makeRequest } = require('./request') | ||
const zlib = require('zlib') | ||
const { | ||
requestBadPort, | ||
responseLocationURL, | ||
requestCurrentURL | ||
requestCurrentURL, | ||
setRequestReferrerPolicyOnRedirect, | ||
makeTimingInfo | ||
} = require('./util') | ||
@@ -22,3 +25,3 @@ const { kState, kHeaders, kGuard } = require('./symbols') | ||
const assert = require('assert') | ||
const { safelyExtractBody, isBodyReadable } = require('./body') | ||
const { safelyExtractBody } = require('./body') | ||
const { | ||
@@ -34,3 +37,9 @@ redirectStatus, | ||
const EE = require('events') | ||
const { PassThrough, pipeline } = require('stream') | ||
// https://fetch.spec.whatwg.org/#garbage-collection | ||
const registry = new FinalizationRegistry((abort) => { | ||
abort() | ||
}) | ||
// https://fetch.spec.whatwg.org/#fetch-method | ||
@@ -40,6 +49,6 @@ async function fetch (resource, init) { | ||
dispatcher: this, | ||
controller: null, | ||
terminated: false, | ||
abort: null, // "connection" abort | ||
terminate ({ aborted } = {}) { | ||
connection: null, | ||
dump: false, | ||
terminate ({ reason, aborted } = {}) { | ||
if (this.terminated) { | ||
@@ -49,4 +58,5 @@ return | ||
if (context.abort) { | ||
context.abort() | ||
if (this.connection) { | ||
this.connection.destroy() | ||
this.connection = null | ||
} | ||
@@ -56,3 +66,3 @@ | ||
this.emit('terminated') | ||
this.emit('terminated', reason) | ||
} | ||
@@ -165,2 +175,57 @@ }) | ||
function finalizeAndReportTiming (response, initiatorType = 'other') { | ||
// 1. If response’s URL list is null or empty, then return. | ||
if (response.urlList.length === 0) { | ||
return | ||
} | ||
// 2. Let originalURL be response’s URL list[0]. | ||
const originalURL = response.urlList[0] | ||
// 3. Let timingInfo be response’s timing info. | ||
let timingInfo = response.timingInfo | ||
// 4. Let cacheState be response’s cache state. | ||
let cacheState = response.cacheState | ||
// 5. If timingInfo is null, then return. | ||
if (timingInfo === null) { | ||
return | ||
} | ||
// 6. If response’s timing allow passed flag is not set, then: | ||
if (!timingInfo.timingAllowPassed) { | ||
// 1. Set timingInfo to a new fetch timing info whose start time and | ||
// post-redirect start time are timingInfo’s start time. | ||
timingInfo = makeTimingInfo({ | ||
startTime: timingInfo.startTime, | ||
postRedirectStartTime: timingInfo.postRedirectStartTime | ||
}) | ||
// 2. Set cacheState to the empty string. | ||
cacheState = '' | ||
} | ||
// 7. Set timingInfo’s end time to the coarsened shared current time | ||
// given global’s relevant settings object’s cross-origin isolated | ||
// capability. | ||
// TODO: given global’s relevant settings object’s cross-origin isolated | ||
// capability | ||
response.timingInfo.endTime = performance.now() | ||
// 8. Set response’s timing info to timingInfo. | ||
response.timingInfo = timingInfo | ||
// 9. Mark resource timing for timingInfo, originalURL, initiatorType, | ||
// global, and cacheState. | ||
markResourceTiming( | ||
timingInfo, | ||
originalURL, | ||
initiatorType, | ||
global, | ||
cacheState | ||
) | ||
} | ||
// https://w3c.github.io/resource-timing/#dfn-mark-resource-timing | ||
function markResourceTiming () { | ||
// TODO | ||
@@ -181,4 +246,4 @@ } | ||
// body with error. | ||
if (request.body !== null && isBodyReadable(request.body)) { | ||
request.body.stream.cancel(error) | ||
if (request.body !== null) { | ||
cancelIfReadable(request.body.stream, error) | ||
} | ||
@@ -196,4 +261,4 @@ | ||
// body with error. | ||
if (response.body != null && isBodyReadable(response.body)) { | ||
context.controller.error(error) | ||
if (response.body != null) { | ||
context.connection.destroy(error) | ||
} | ||
@@ -222,15 +287,6 @@ } | ||
const currenTime = performance.now() | ||
const timingInfo = { | ||
const timingInfo = makeTimingInfo({ | ||
startTime: currenTime, | ||
redirectStartTime: 0, | ||
redirectEndTime: 0, | ||
postRedirectStartTime: currenTime, | ||
finalServiceWorkerStartTime: 0, | ||
finalNetworkResponseStartTime: 0, | ||
finalNetworkRequestStartTime: 0, | ||
endTime: 0, | ||
encodedBodySize: 0, | ||
decodedBodySize: 0, | ||
finalConnectionTimingInfo: null | ||
} | ||
postRedirectStartTime: currenTime | ||
}) | ||
@@ -317,2 +373,4 @@ // 6. Let fetchParams be a new fetch params whose request is request, timing | ||
async function mainFetch (fetchParams, recursive = false) { | ||
const context = this | ||
// 1. Let request be fetchParams’s request. | ||
@@ -528,5 +586,3 @@ const request = fetchParams.request | ||
internalResponse.body = null | ||
if (context.controller) { | ||
context.controller.error(new AbortError()) | ||
} | ||
context.connection.dump = true | ||
} | ||
@@ -583,3 +639,3 @@ | ||
// fetchParams’s task destination. | ||
if (fetchParams.processResponseDone) { | ||
if (fetchParams.processResponseDone !== null) { | ||
fetchParams.processResponseDone(response) | ||
@@ -591,2 +647,4 @@ } | ||
function fetchFinale (fetchParams, response) { | ||
const context = this | ||
// 1. If fetchParams’s process response is non-null, | ||
@@ -613,2 +671,8 @@ // then queue a fetch task to run fetchParams’s process response | ||
// TODO | ||
// TODO (spec): The spec doesn't specify this but we need to | ||
// terminate fetch if we have an error response. | ||
if (response.status === 0) { | ||
context.terminate({ reason: response.error }) | ||
} | ||
} | ||
@@ -618,2 +682,4 @@ | ||
async function httpFetch (fetchParams) { | ||
const context = this | ||
// 1. Let request be fetchParams’s request. | ||
@@ -673,5 +739,3 @@ const request = fetchParams.request | ||
// See, https://github.com/whatwg/fetch/issues/1288 | ||
if (context.abort) { | ||
context.abort() | ||
} | ||
context.connection.destroy() | ||
@@ -815,3 +879,4 @@ // 2. Switch on request’s redirect mode: | ||
// 16. Set timingInfo’s redirect end time and post-redirect start time to the | ||
// coarsened shared current time given fetchParams’s cross-origin isolated capability. | ||
// coarsened shared current time given fetchParams’s cross-origin isolated | ||
// capability. | ||
// TODO: given fetchParams’s cross-origin isolated capability? | ||
@@ -821,4 +886,4 @@ timingInfo.redirectEndTime = timingInfo.postRedirectStartTime = | ||
// 17. If timingInfo’s redirect start time is 0, then set timingInfo’s redirect start | ||
// time to timingInfo’s start time. | ||
// 17. If timingInfo’s redirect start time is 0, then set timingInfo’s | ||
// redirect start time to timingInfo’s start time. | ||
if (timingInfo.redirectStartTime === 0) { | ||
@@ -831,4 +896,5 @@ timingInfo.redirectStartTime = timingInfo.startTime | ||
// 19. Invoke set request’s referrer policy on redirect on request and actualResponse. | ||
// TODO | ||
// 19. Invoke set request’s referrer policy on redirect on request and | ||
// actualResponse. | ||
setRequestReferrerPolicyOnRedirect(request, actualResponse) | ||
@@ -845,2 +911,4 @@ // 20. Return the result of running main fetch given fetchParams and true. | ||
) { | ||
const context = this | ||
// 1. Let request be fetchParams’s request. | ||
@@ -1155,2 +1223,4 @@ const request = fetchParams.request | ||
) { | ||
// then: | ||
// 1. If the ongoing fetch is terminated, then: | ||
@@ -1168,2 +1238,10 @@ if (context.terminated) { | ||
// 2. Set response to the result of running HTTP-network-or-cache | ||
// fetch given fetchParams, isAuthenticationFetch, and true. | ||
// TODO (spec): The spec doesn't specify this but we need to cancel | ||
// the active response before we can start a new one. | ||
// https://github.com/whatwg/fetch/issues/1293 | ||
context.connection.destroy() | ||
response = await httpNetworkOrCacheFetch.call( | ||
@@ -1193,5 +1271,43 @@ this, | ||
) { | ||
const context = this | ||
return new Promise((resolve) => { | ||
const context = this | ||
assert(!context.connection || context.connection.destroyed) | ||
const connection = (context.connection = { | ||
abort: null, | ||
controller: null, | ||
destroyed: false, | ||
errored: false, | ||
dump: false, | ||
destroy (err) { | ||
if (this.destroyed) { | ||
return | ||
} | ||
this.destroyed = true | ||
if (this.abort) { | ||
this.abort() | ||
this.abort = null | ||
} | ||
if (err) { | ||
this.errored = err | ||
} | ||
if (this.controller) { | ||
try { | ||
this.controller.error(err ?? new AbortError()) | ||
this.controller = null | ||
} catch (err) { | ||
// Will throw TypeError if body is not readable. | ||
if (err.name !== 'TypeError') { | ||
throw err | ||
} | ||
} | ||
} | ||
} | ||
}) | ||
// 1. Let request be fetchParams’s request. | ||
@@ -1204,3 +1320,3 @@ const request = fetchParams.request | ||
// 3. Let timingInfo be fetchParams’s timing info. | ||
// TODO | ||
const timingInfo = fetchParams.timingInfo | ||
@@ -1312,12 +1428,11 @@ // 4. Let httpCache be the result of determining the HTTP cache partition, | ||
// 2. If e is an "AbortError" DOMException, then terminate the ongoing fetch with the aborted flag set. | ||
if (e.name === 'AbortError') { | ||
context.terminate({ aborted: true }) | ||
return | ||
} | ||
// 3. Otherwise, terminate the ongoing fetch. | ||
context.terminate() | ||
context.terminate({ | ||
aborted: e.name === 'AbortError', | ||
reason: e | ||
}) | ||
} | ||
})() | ||
// 9. If aborted, then: | ||
function onRequestAborted () { | ||
@@ -1328,26 +1443,99 @@ // 1. Let aborted be the termination’s aborted flag. | ||
// 2. If connection uses HTTP/2, then transmit an RST_STREAM frame. | ||
if (context.abort) { | ||
context.abort() | ||
} | ||
connection.destroy() | ||
// 3. If aborted is set, then return an aborted network error. | ||
if (aborted) { | ||
resolve(makeNetworkError(new AbortError())) | ||
} | ||
const reason = aborted ? new AbortError() : new Error('terminated') | ||
// 4. Return a network error. | ||
resolve(makeNetworkError()) | ||
resolve(makeNetworkError(reason)) | ||
} | ||
// TODO... | ||
// 10. Let pullAlgorithm be an action that resumes the ongoing fetch | ||
// if it is suspended. | ||
let pullAlgorithm | ||
// TODO: forceNewConnection | ||
// 11. Let cancelAlgorithm be an action that terminates the ongoing | ||
// fetch with the aborted flag set. | ||
const cancelAlgorithm = () => { | ||
context.terminate({ aborted: true }) | ||
} | ||
// NOTE: This is just a hack. | ||
if (forceNewConnection && context.controller) { | ||
context.abort() | ||
// 12. Let highWaterMark be a non-negative, non-NaN number, chosen by | ||
// the user agent. | ||
const highWaterMark = 65536 | ||
// 13. Let sizeAlgorithm be an algorithm that accepts a chunk object | ||
// and returns a non-negative, non-NaN, non-infinite number, chosen by the user agent. | ||
// TODO | ||
// 14. Let stream be a new ReadableStream. | ||
// 15. Set up stream with pullAlgorithm set to pullAlgorithm, | ||
// cancelAlgorithm set to cancelAlgorithm, highWaterMark set to | ||
// highWaterMark, and sizeAlgorithm set to sizeAlgorithm. | ||
const stream = new ReadableStream( | ||
{ | ||
async start (controller) { | ||
connection.controller = controller | ||
}, | ||
async pull () { | ||
if (pullAlgorithm) { | ||
pullAlgorithm() | ||
} else { | ||
pullAlgorithm = null | ||
} | ||
}, | ||
async cancel (reason) { | ||
cancelAlgorithm() | ||
} | ||
}, | ||
{ highWaterMark } | ||
) | ||
// 16. Run these steps, but abort when the ongoing fetch is terminated: | ||
// TODO | ||
// 17. If aborted, then: | ||
// TODO: How can this happen? The steps above are not async? | ||
// 18. Run these steps in parallel: | ||
// 1. Run these steps, but abort when the ongoing fetch is terminated: | ||
// 1. While true: | ||
// 1. If one or more bytes have been transmitted from response’s | ||
// message body, then: | ||
// NOTE: See onHeaders | ||
// 2. Otherwise, if the bytes transmission for response’s message | ||
// body is done normally and stream is readable, then close stream, | ||
// finalize response for fetchParams and response, and abort these | ||
// in-parallel steps. | ||
// NOTE: See onHeaders | ||
// 2. If aborted, then: | ||
function onResponseAborted () { | ||
// 1. Finalize response for fetchParams and response. | ||
finalizeResponse(fetchParams, response) | ||
// 2. Let aborted be the termination’s aborted flag. | ||
const aborted = context.terminated.aborted | ||
// 3. If aborted is set, then: | ||
if (aborted) { | ||
// 1. Set response’s aborted flag. | ||
response.aborted = true | ||
// 2. If stream is readable, error stream with an "AbortError" DOMException. | ||
connection.destroy(new AbortError()) | ||
} else { | ||
// 4. Otherwise, if stream is readable, error stream with a TypeError. | ||
connection.destroy(new TypeError('terminated')) | ||
} | ||
// 5. If connection uses HTTP/2, then transmit an RST_STREAM frame. | ||
// 6. Otherwise, the user agent should close connection unless it would be bad for performance to do so. | ||
connection.destroy() | ||
} | ||
assert(!context.controller) | ||
// 19. Return response. | ||
// NOTE: See onHeaders | ||
// Implementation | ||
const url = requestCurrentURL(request) | ||
@@ -1364,7 +1552,9 @@ context.dispatcher.dispatch( | ||
{ | ||
decoder: null, | ||
onConnect (abort) { | ||
if (context.terminated) { | ||
if (connection.destroyed) { | ||
abort(new AbortError()) | ||
} else { | ||
context.abort = (err) => abort(err ?? new AbortError()) | ||
connection.abort = abort | ||
} | ||
@@ -1386,28 +1576,6 @@ }, | ||
const stream = | ||
status === 204 | ||
? null | ||
: new ReadableStream( | ||
{ | ||
async start (controller) { | ||
context.controller = controller | ||
}, | ||
async pull () { | ||
resume() | ||
}, | ||
async cancel (reason) { | ||
let err | ||
if (reason instanceof Error) { | ||
err = reason | ||
} else if (typeof reason === 'string') { | ||
err = new Error(reason) | ||
} else { | ||
err = new AbortError() | ||
} | ||
const hasPulled = pullAlgorithm !== undefined | ||
context.abort(err) | ||
} | ||
}, | ||
{ highWaterMark: 16384 } | ||
) | ||
const body = { stream } | ||
registry.register(body, connection.abort) | ||
@@ -1418,40 +1586,156 @@ response = makeResponse({ | ||
headersList: headers[kHeadersList], | ||
body: stream ? { stream } : null | ||
body | ||
}) | ||
context.on('terminated', onResponseAborted) | ||
const codings = | ||
headers | ||
.get('content-encoding') | ||
?.toLowerCase() | ||
.split(',') | ||
.map((x) => x.trim()) ?? [] | ||
const decoders = [] | ||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding | ||
for (const coding of codings) { | ||
if (/(x-)?gzip/.test(coding)) { | ||
decoders.push(zlib.createGunzip()) | ||
} else if (/(x-)?deflate/.test(coding)) { | ||
decoders.push(zlib.createInflate()) | ||
} else if (coding === 'br') { | ||
decoders.push(zlib.createBrotliDecompress()) | ||
} else { | ||
// TODO: What to do when coding is invalid or unsupported? | ||
} | ||
} | ||
let iterator | ||
if (decoders.length > 1) { | ||
this.decoder = new PassThrough() | ||
iterator = pipeline(this.decoder, ...decoders, () => {})[ | ||
Symbol.asyncIterator | ||
]() | ||
} else if (decoders.length === 1) { | ||
this.decoder = decoders[0] | ||
iterator = this.decoder[Symbol.asyncIterator]() | ||
} else { | ||
this.decoder = new PassThrough() | ||
iterator = this.decoder[Symbol.asyncIterator]() | ||
} | ||
if (this.decoder) { | ||
this.decoder.on('drain', resume) | ||
} | ||
pullAlgorithm = async () => { | ||
// 4. Set bytes to the result of handling content codings given | ||
// codings and bytes. | ||
let bytes | ||
try { | ||
const { done, value } = await iterator.next() | ||
bytes = done ? undefined : value | ||
} catch (err) { | ||
if (this.decoder.writableEnded && !timingInfo.encodedBodySize) { | ||
// zlib doesn't like empty streams. | ||
bytes = undefined | ||
} else { | ||
bytes = err | ||
} | ||
} | ||
if (!connection.controller) { | ||
return | ||
} | ||
if (bytes === undefined) { | ||
// 2. Otherwise, if the bytes transmission for response’s message | ||
// body is done normally and stream is readable, then close | ||
// stream, finalize response for fetchParams and response, and | ||
// abort these in-parallel steps. | ||
finalizeResponse(fetchParams, response) | ||
context.off('terminated', onResponseAborted) | ||
context.off('terminated', onRequestAborted) | ||
connection.controller.close() | ||
connection.controller = null | ||
connection.destroy() | ||
return | ||
} | ||
// 5. Increase timingInfo’s decoded body size by bytes’s length. | ||
timingInfo.decodedBodySize += bytes ? bytes.byteLength : 0 | ||
// 6. If bytes is failure, then terminate the ongoing fetch. | ||
if (bytes instanceof Error) { | ||
context.terminate({ reason: bytes }) | ||
return | ||
} | ||
// 7. Enqueue a Uint8Array wrapping an ArrayBuffer containing bytes | ||
// into stream. | ||
connection.controller.enqueue(new Uint8Array(bytes)) | ||
// 8. If stream is errored, then terminate the ongoing fetch. | ||
if (connection.errored) { | ||
context.terminate({ reason: connection.errored }) | ||
return | ||
} | ||
// 9. If stream doesn’t need more data ask the user agent to suspend | ||
// the ongoing fetch. | ||
return connection.controller.desiredSize > 0 | ||
} | ||
if (hasPulled) { | ||
pullAlgorithm() | ||
} | ||
resolve(response) | ||
return false | ||
return true | ||
}, | ||
onData (chunk) { | ||
assert(context.controller) | ||
if (connection.dump) { | ||
return | ||
} | ||
// Copy the Buffer to detach it from Buffer pool. | ||
// TODO: Is this required? | ||
chunk = new Uint8Array(chunk) | ||
// 1. If one or more bytes have been transmitted from response’s | ||
// message body, then: | ||
context.controller.enqueue(chunk) | ||
// 1. Let bytes be the transmitted bytes. | ||
const bytes = chunk | ||
return context.controller.desiredSize > 0 | ||
}, | ||
// 2. Let codings be the result of extracting header list values | ||
// given `Content-Encoding` and response’s header list. | ||
// See pullAlgorithm. | ||
onComplete () { | ||
assert(context.controller) | ||
// 3. Increase timingInfo’s encoded body size by bytes’s length. | ||
timingInfo.encodedBodySize += bytes.byteLength | ||
context.controller.close() | ||
context.controller = null | ||
// 4. See pullAlgorithm... | ||
finalizeResponse(fetchParams, response) | ||
return this.decoder.write(bytes) | ||
}, | ||
onError (err) { | ||
context.terminate({ aborted: err.name === 'AbortError' }) | ||
async onComplete () { | ||
this.decoder.end() | ||
}, | ||
if (context.controller) { | ||
context.controller.error(err) | ||
context.controller = null | ||
} else { | ||
// TODO: What if 204? | ||
resolve(makeNetworkError(err)) | ||
onError (error) { | ||
context.off('terminated', onResponseAborted) | ||
context.off('terminated', onRequestAborted) | ||
connection.destroy(error) | ||
context.terminate({ reason: error }) | ||
if (!response) { | ||
resolve(makeNetworkError(error)) | ||
} | ||
@@ -1475,2 +1759,13 @@ } | ||
function cancelIfReadable (stream, reason) { | ||
try { | ||
stream.cancel(reason) | ||
} catch (err) { | ||
// Will throw TypeError if body is not readable. | ||
if (err.name !== 'TypeError') { | ||
throw err | ||
} | ||
} | ||
} | ||
module.exports = fetch |
@@ -278,2 +278,3 @@ 'use strict' | ||
timingInfo: null, | ||
cacheState: '', | ||
statusText: '', | ||
@@ -280,0 +281,0 @@ ...init, |
@@ -205,4 +205,40 @@ 'use strict' | ||
// https://w3c.github.io/webappsec-referrer-policy/#set-requests-referrer-policy-on-redirect | ||
function setRequestReferrerPolicyOnRedirect (request, actualResponse) { | ||
// Given a request request and a response actualResponse, this algorithm | ||
// updates request’s referrer policy according to the Referrer-Policy | ||
// header (if any) in actualResponse. | ||
// 1. Let policy be the result of executing § 8.1 Parse a referrer policy | ||
// from a Referrer-Policy header on actualResponse. | ||
// TODO: https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header | ||
const policy = '' | ||
// 2. If policy is not the empty string, then set request’s referrer policy to policy. | ||
if (policy !== '') { | ||
request.referrerPolicy = policy | ||
} | ||
} | ||
function makeTimingInfo (init) { | ||
return { | ||
startTime: 0, | ||
redirectStartTime: 0, | ||
redirectEndTime: 0, | ||
postRedirectStartTime: 0, | ||
finalServiceWorkerStartTime: 0, | ||
finalNetworkResponseStartTime: 0, | ||
finalNetworkRequestStartTime: 0, | ||
endTime: 0, | ||
encodedBodySize: 0, | ||
decodedBodySize: 0, | ||
finalConnectionTimingInfo: null, | ||
...init | ||
} | ||
} | ||
module.exports = { | ||
toWebReadable, | ||
makeTimingInfo, | ||
setRequestReferrerPolicyOnRedirect, | ||
isValidHTTPToken, | ||
@@ -209,0 +245,0 @@ requestBadPort, |
{ | ||
"name": "undici", | ||
"version": "4.4.1", | ||
"version": "4.4.2", | ||
"description": "An HTTP/1.1 client, written from scratch for Node.js", | ||
@@ -5,0 +5,0 @@ "homepage": "https://undici.nodejs.org", |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
567818
8661