pax_global_header00006660000000000000000000000064144577333300014523gustar00rootroot0000000000000052 comment=8b3320d2a7c07bce4afc6b2bf6c3bbddda85b01f node-fetch-3.3.2/000077500000000000000000000000001445773333000135445ustar00rootroot00000000000000node-fetch-3.3.2/.editorconfig000066400000000000000000000004561445773333000162260ustar00rootroot00000000000000# editorconfig.org root = true [*] end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true indent_style = tab [*.md] trim_trailing_whitespace = false [*.yml] indent_style = space [package.json] indent_style = space indent_size = 2 insert_final_newline = false node-fetch-3.3.2/.github/000077500000000000000000000000001445773333000151045ustar00rootroot00000000000000node-fetch-3.3.2/.github/FUNDING.yml000066400000000000000000000013041445773333000167170ustar00rootroot00000000000000# These are supported funding model platforms github: node-fetch # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] patreon: # Replace with a single Patreon username open_collective: node-fetch # Replace with a single Open Collective username ko_fi: # Replace with a single Ko-fi username tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username custom: # Replace with a single custom sponsorship URL node-fetch-3.3.2/.github/ISSUE_TEMPLATE/000077500000000000000000000000001445773333000172675ustar00rootroot00000000000000node-fetch-3.3.2/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000016241445773333000217640ustar00rootroot00000000000000--- name: 🞠Bug report about: Create a report to help us improve node-fetch labels: bug --- **Reproduction** Steps to reproduce the behavior: 1. 2. 3. 4. **Expected behavior** **Screenshots** **Your Environment** | software | version | ---------------- | ------- | node-fetch | | node | | npm | | Operating System | **Additional context** node-fetch-3.3.2/.github/ISSUE_TEMPLATE/config.yml000066400000000000000000000007221445773333000212600ustar00rootroot00000000000000blank_issues_enabled: true contact_links: - name: ✨ Feature Request url: https://github.com/node-fetch/node-fetch/discussions/new?category=ideas about: Suggest an idea or feature - name: 🤔 Support or Usage Question url: https://github.com/node-fetch/node-fetch/discussions/new?category=q-a about: Get help using node-fetch - name: Discord Server url: https://discord.gg/Zxbndcm about: You can alternatively ask any questions here. node-fetch-3.3.2/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000005531445773333000207100ustar00rootroot00000000000000 ## Purpose ## Changes ## Additional information ___ - [ ] I updated readme - [ ] I added unit test(s) ___ - fix #000 node-fetch-3.3.2/.github/dependabot.yml000066400000000000000000000005261445773333000177370ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: npm directory: "/" schedule: interval: daily open-pull-requests-limit: 10 ignore: - dependency-name: formdata-node versions: - 3.0.0 - 3.1.0 - dependency-name: xo versions: - 0.37.1 - 0.38.1 - 0.38.2 - dependency-name: p-timeout versions: - 4.1.0 node-fetch-3.3.2/.github/semantic.yml000066400000000000000000000002071445773333000174310ustar00rootroot00000000000000# https://github.com/zeke/semantic-pull-requests#configuration # Always validate the PR title, and ignore the commits titleOnly: true node-fetch-3.3.2/.github/workflows/000077500000000000000000000000001445773333000171415ustar00rootroot00000000000000node-fetch-3.3.2/.github/workflows/ci.yml000066400000000000000000000016041445773333000202600ustar00rootroot00000000000000name: CI on: push: branches: [main] pull_request: paths: - "**.js" - "package.json" - ".github/workflows/ci.yml" jobs: test: strategy: matrix: os: [ubuntu-latest, windows-latest, macOS-latest] node: ["12.20.0", "14.13.1", "16.0.0"] exclude: # On Windows, run tests with only the LTS environments. - os: windows-latest node: "12.22.3" - os: windows-latest node: "16.0.0" # On macOS, run tests with only the LTS environments. - os: macOS-latest node: "12.22.3" - os: macOS-latest node: "16.0.0" runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: ${{ matrix.node }} - run: npm install - run: npm test -- --colors node-fetch-3.3.2/.github/workflows/lint.yml000066400000000000000000000005721445773333000206360ustar00rootroot00000000000000name: CI on: pull_request: paths: - "**.js" - "**eslint**" - "package.json" - ".github/workflows/lint.yml" jobs: lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - name: Use Node.js uses: actions/setup-node@v2 with: node-version: 14 - run: npm install - run: npm run lint node-fetch-3.3.2/.github/workflows/release.yml000066400000000000000000000007171445773333000213110ustar00rootroot00000000000000name: Release on: push: branches: - main - next - beta - "*.x" # maintenance releases such as 2.x jobs: release: name: release runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: "lts/*" - run: npx semantic-release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} NPM_TOKEN: ${{ secrets.NPM_TOKEN }} node-fetch-3.3.2/.github/workflows/types.yml000066400000000000000000000005171445773333000210330ustar00rootroot00000000000000name: CI on: pull_request: paths: - "**.ts" - package.json - .github/workflows/types.yml jobs: typescript: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 - run: npm install - name: Check typings file run: npm run test-types node-fetch-3.3.2/.gitignore000066400000000000000000000014111445773333000155310ustar00rootroot00000000000000# Sketch temporary file ~*.sketch # Logs logs *.log # Runtime data pids *.pid *.seed # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like nyc and istanbul .nyc_output coverage # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Compiled binary addons (http://nodejs.org/api/addons.html) build/Release # Dependency directory # Commenting this out is preferred by some people, see # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- node_modules # Users Environment Variables .lock-wscript # OS files .DS_Store # Babel-compiled files lib # Ignore package manager lock files package-lock.json yarn.lock # Ignore IDE .idea node-fetch-3.3.2/.npmrc000066400000000000000000000000441445773333000146620ustar00rootroot00000000000000package-lock=false save-exact=false node-fetch-3.3.2/@types/000077500000000000000000000000001445773333000150105ustar00rootroot00000000000000node-fetch-3.3.2/@types/index.d.ts000066400000000000000000000147561445773333000167260ustar00rootroot00000000000000/// import {RequestOptions} from 'http'; import {FormData} from 'formdata-polyfill/esm.min.js'; import { Blob, blobFrom, blobFromSync, File, fileFrom, fileFromSync } from 'fetch-blob/from.js'; type AbortSignal = { readonly aborted: boolean; addEventListener: (type: 'abort', listener: (this: AbortSignal) => void) => void; removeEventListener: (type: 'abort', listener: (this: AbortSignal) => void) => void; }; export type HeadersInit = Headers | Record | Iterable | Iterable>; export { FormData, Blob, blobFrom, blobFromSync, File, fileFrom, fileFromSync }; /** * This Fetch API interface allows you to perform various actions on HTTP request and response headers. * These actions include retrieving, setting, adding to, and removing. * A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs. * You can add to this using methods like append() (see Examples.) * In all methods of this interface, header names are matched by case-insensitive byte sequence. * */ export class Headers { constructor(init?: HeadersInit); append(name: string, value: string): void; delete(name: string): void; get(name: string): string | null; has(name: string): boolean; set(name: string, value: string): void; forEach( callbackfn: (value: string, key: string, parent: Headers) => void, thisArg?: any ): void; [Symbol.iterator](): IterableIterator<[string, string]>; /** * Returns an iterator allowing to go through all key/value pairs contained in this object. */ entries(): IterableIterator<[string, string]>; /** * Returns an iterator allowing to go through all keys of the key/value pairs contained in this object. */ keys(): IterableIterator; /** * Returns an iterator allowing to go through all values of the key/value pairs contained in this object. */ values(): IterableIterator; /** Node-fetch extension */ raw(): Record; } export interface RequestInit { /** * A BodyInit object or null to set request's body. */ body?: BodyInit | null; /** * A Headers object, an object literal, or an array of two-item arrays to set request's headers. */ headers?: HeadersInit; /** * A string to set request's method. */ method?: string; /** * A string indicating whether request follows redirects, results in an error upon encountering a redirect, or returns the redirect (in an opaque fashion). Sets request's redirect. */ redirect?: RequestRedirect; /** * An AbortSignal to set request's signal. */ signal?: AbortSignal | null; /** * A string whose value is a same-origin URL, "about:client", or the empty string, to set request’s referrer. */ referrer?: string; /** * A referrer policy to set request’s referrerPolicy. */ referrerPolicy?: ReferrerPolicy; // Node-fetch extensions to the whatwg/fetch spec agent?: RequestOptions['agent'] | ((parsedUrl: URL) => RequestOptions['agent']); compress?: boolean; counter?: number; follow?: number; hostname?: string; port?: number; protocol?: string; size?: number; highWaterMark?: number; insecureHTTPParser?: boolean; } export interface ResponseInit { headers?: HeadersInit; status?: number; statusText?: string; } export type BodyInit = | Blob | Buffer | URLSearchParams | FormData | NodeJS.ReadableStream | string; declare class BodyMixin { constructor(body?: BodyInit, options?: {size?: number}); readonly body: NodeJS.ReadableStream | null; readonly bodyUsed: boolean; readonly size: number; /** @deprecated Use `body.arrayBuffer()` instead. */ buffer(): Promise; arrayBuffer(): Promise; formData(): Promise; blob(): Promise; json(): Promise; text(): Promise; } // `Body` must not be exported as a class since it's not exported from the JavaScript code. export interface Body extends Pick {} export type RequestRedirect = 'error' | 'follow' | 'manual'; export type ReferrerPolicy = '' | 'no-referrer' | 'no-referrer-when-downgrade' | 'same-origin' | 'origin' | 'strict-origin' | 'origin-when-cross-origin' | 'strict-origin-when-cross-origin' | 'unsafe-url'; export type RequestInfo = string | Request; export class Request extends BodyMixin { constructor(input: URL | RequestInfo, init?: RequestInit); /** * Returns a Headers object consisting of the headers associated with request. Note that headers added in the network layer by the user agent will not be accounted for in this object, e.g., the "Host" header. */ readonly headers: Headers; /** * Returns request's HTTP method, which is "GET" by default. */ readonly method: string; /** * Returns the redirect mode associated with request, which is a string indicating how redirects for the request will be handled during fetching. A request will follow redirects by default. */ readonly redirect: RequestRedirect; /** * Returns the signal associated with request, which is an AbortSignal object indicating whether or not request has been aborted, and its abort event handler. */ readonly signal: AbortSignal; /** * Returns the URL of request as a string. */ readonly url: string; /** * A string whose value is a same-origin URL, "about:client", or the empty string, to set request’s referrer. */ readonly referrer: string; /** * A referrer policy to set request’s referrerPolicy. */ readonly referrerPolicy: ReferrerPolicy; clone(): Request; } type ResponseType = 'basic' | 'cors' | 'default' | 'error' | 'opaque' | 'opaqueredirect'; export class Response extends BodyMixin { constructor(body?: BodyInit | null, init?: ResponseInit); readonly headers: Headers; readonly ok: boolean; readonly redirected: boolean; readonly status: number; readonly statusText: string; readonly type: ResponseType; readonly url: string; clone(): Response; static error(): Response; static redirect(url: string, status?: number): Response; static json(data: any, init?: ResponseInit): Response; } export class FetchError extends Error { constructor(message: string, type: string, systemError?: Record); name: 'FetchError'; [Symbol.toStringTag]: 'FetchError'; type: string; code?: string; errno?: string; } export class AbortError extends Error { type: string; name: 'AbortError'; [Symbol.toStringTag]: 'AbortError'; } export function isRedirect(code: number): boolean; export default function fetch(url: URL | RequestInfo, init?: RequestInit): Promise; node-fetch-3.3.2/@types/index.test-d.ts000066400000000000000000000064151445773333000176740ustar00rootroot00000000000000import {expectType, expectAssignable} from 'tsd'; import AbortController from 'abort-controller'; import Blob from 'fetch-blob'; import fetch, {Request, Response, Headers, Body, FetchError, AbortError} from '.'; import * as _fetch from '.'; async function run() { const getResponse = await fetch('https://bigfile.com/test.zip'); await fetch(new URL('https://bigfile.com/test.zip')); expectType(getResponse.ok); expectType(getResponse.size); expectType(getResponse.status); expectType(getResponse.statusText); expectType<() => Response>(getResponse.clone); // Test async iterator over body expectType(getResponse.body); if (getResponse.body) { for await (const data of getResponse.body) { expectType(data); } } // Test Buffer expectType(await getResponse.buffer()); // Test arrayBuffer expectType(await getResponse.arrayBuffer()); // Test JSON, returns unknown expectType(await getResponse.json()); // Headers iterable expectType(getResponse.headers); // Post try { const request = new Request('http://byjka.com/buka'); new Request(new URL('http://byjka.com/buka')); expectType(request.url); expectType(request.headers); const headers = new Headers({byaka: 'buke'}); expectType<(a: string, b: string) => void>(headers.append); expectType<(a: string) => string | null>(headers.get); expectType<(name: string, value: string) => void>(headers.set); expectType<(name: string) => void>(headers.delete); expectType<() => IterableIterator>(headers.keys); expectType<() => IterableIterator<[string, string]>>(headers.entries); expectType<() => IterableIterator<[string, string]>>(headers[Symbol.iterator]); const postResponse = await fetch(request, {method: 'POST', headers}); expectType(await postResponse.blob()); } catch (error: unknown) { if (error instanceof FetchError) { throw new TypeError(error.errno as string | undefined); } if (error instanceof AbortError) { throw error; } } // export * const wildResponse = await _fetch.default('https://google.com'); expectType(wildResponse.ok); expectType(wildResponse.size); expectType(wildResponse.status); expectType(wildResponse.statusText); expectType<() => Response>(wildResponse.clone); // Others const response = new Response(); expectType(response.url); expectAssignable(response); const abortController = new AbortController(); const request = new Request('url', {signal: abortController.signal}); expectAssignable(request); /* eslint-disable no-new */ new Request('url', {agent: false}); new Headers({Header: 'value'}); // new Headers(['header', 'value']); // should not work new Headers([['header', 'value']]); new Headers(new Headers()); new Headers([ new Set(['a', '1']), ['b', '2'], new Map([['a', null], ['3', null]]).keys() ]); /* eslint-enable no-new */ expectType(Response.redirect('https://google.com')); expectType(Response.redirect('https://google.com', 301)); expectType(Response.json({foo: 'bar'})); expectType(Response.json({foo: 'bar'}, { status: 301 })); } run().finally(() => { console.log('✅'); }); node-fetch-3.3.2/CODE_OF_CONDUCT.md000066400000000000000000000064241445773333000163510ustar00rootroot00000000000000# Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at jimmy@warting.se. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq node-fetch-3.3.2/CONTRIBUTING.md000066400000000000000000000076351445773333000160100ustar00rootroot00000000000000# Contributing Thank you for considering to contribute to `node-fetch` 💖 Please note that this project is released with a [Contributor Code of Conduct](CODE_OF_CONDUCT.md). By participating you agree to abide by its terms. ## Setup Node.js 12.20 or higher is required. Install it from https://nodejs.org/en/. [GitHub's `gh` CLI](https://cli.github.com/) is recommended for the initial setup 1. Fork this repository and clone it to your local machine. Using `gh` you can do this ``` gh repo fork node-fetch/node-fetch ``` 2. After cloning and changing into the `node-fetch` directory, install dependencies and run the tests ``` npm install npm test ``` ## Issues before pull requests Unless the change is trivial such as a type, please [open an issue first](https://github.com/node-fetch/node-fetch/issues/new) before starting a pull request for a bug fix or a new feature. After you cloned your fork, create a new branch and implement the changes in them. To start a pull request, you can use the `gh` CLI ``` gh pr create ``` ## Maintainers only ### Merging the Pull Request & releasing a new version Releases are automated using [semantic-release](https://github.com/semantic-release/semantic-release). The following commit message conventions determine which version is released: 1. `fix: ...` or `fix(scope name): ...` prefix in subject: bumps fix version, e.g. `1.2.3` → `1.2.4` 2. `feat: ...` or `feat(scope name): ...` prefix in subject: bumps feature version, e.g. `1.2.3` → `1.3.0` 3. `BREAKING CHANGE:` in body: bumps breaking version, e.g. `1.2.3` → `2.0.0` Only one version number is bumped at a time, the highest version change trumps the others. Besides, publishing a new version to npm, semantic-release also creates a git tag and release on GitHub, generates changelogs from the commit messages and puts them into the release notes. If the pull request looks good but does not follow the commit conventions, update the pull request title and use the Squash & merge button, at which point you can set a custom commit message. ### Beta/Next/Maintenance releases `semantic-release` supports pre-releases and maintenance releases. In order to release a maintenance version, open a pull request against a `[VERSION].x` branch, e.g. `2.x`. As long as the commit conventions documented above are followed, a maintenance version will be released. Breaking changes are not permitted. In order to release a beta version, create or re-create the `beta` branch based on the latest `main` branch. Then create a pull request against the `beta` branch. When merged into the `beta` branch, a new `...-beta.X` release will be created. Once ready, create a pull request against `main` (or `next` if you prefer). This pull request be merged using `squash & merge`. **Important**: do not rebase & force push to the `beta` branch while working on pre-releases. Do only use force push to reset the `beta` branch in order to sync it with the latest `main`. To release a `next` version, create or re-create the `next` branch based on the latest `main` branch. Then create a pull request against the `next` branch. When merged into the `next` branch, a new version is created based on the commit conventions, but published using the `next` dist-tag to npm and marked as pre-release on GitHub. **Important**: do not rebase & force push to the `next` branch while working on pre-releases. Do only use force push to reset the `next` branch in order to sync it with the latest `main`. Also, when merging `next` into `main`, **do not use squash & merge**! In this particular case, the traditional "Create a merge commit" merge button has to be used, otherwise semantic-release will not be able to match the commit history and won't be able to promote the existing releases in npm or GitHub. If the button is disabled, temporarily enable it in the repository settings. For any semantic-release questions, ping [@gr2m](https://github.com/gr2m). node-fetch-3.3.2/LICENSE.md000066400000000000000000000021021445773333000151430ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 - 2020 Node Fetch Team 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. node-fetch-3.3.2/README.md000066400000000000000000000706651445773333000150410ustar00rootroot00000000000000
Node Fetch

A light-weight module that brings Fetch API to Node.js.

Build status Coverage status Current version Install size Mentioned in Awesome Node.js Discord

Consider supporting us on our Open Collective:

Open Collective
--- **You might be looking for the [v2 docs](https://github.com/node-fetch/node-fetch/tree/2.x#readme)** - [Motivation](#motivation) - [Features](#features) - [Difference from client-side fetch](#difference-from-client-side-fetch) - [Installation](#installation) - [Loading and configuring the module](#loading-and-configuring-the-module) - [Upgrading](#upgrading) - [Common Usage](#common-usage) - [Plain text or HTML](#plain-text-or-html) - [JSON](#json) - [Simple Post](#simple-post) - [Post with JSON](#post-with-json) - [Post with form parameters](#post-with-form-parameters) - [Handling exceptions](#handling-exceptions) - [Handling client and server errors](#handling-client-and-server-errors) - [Handling cookies](#handling-cookies) - [Advanced Usage](#advanced-usage) - [Streams](#streams) - [Accessing Headers and other Metadata](#accessing-headers-and-other-metadata) - [Extract Set-Cookie Header](#extract-set-cookie-header) - [Post data using a file](#post-data-using-a-file) - [Request cancellation with AbortSignal](#request-cancellation-with-abortsignal) - [API](#api) - [fetch(url[, options])](#fetchurl-options) - [Options](#options) - [Default Headers](#default-headers) - [Custom Agent](#custom-agent) - [Custom highWaterMark](#custom-highwatermark) - [Insecure HTTP Parser](#insecure-http-parser) - [Class: Request](#class-request) - [new Request(input[, options])](#new-requestinput-options) - [Class: Response](#class-response) - [new Response([body[, options]])](#new-responsebody-options) - [response.ok](#responseok) - [response.redirected](#responseredirected) - [response.type](#responsetype) - [Class: Headers](#class-headers) - [new Headers([init])](#new-headersinit) - [Interface: Body](#interface-body) - [body.body](#bodybody) - [body.bodyUsed](#bodybodyused) - [body.arrayBuffer()](#bodyarraybuffer) - [body.blob()](#bodyblob) - [body.formData()](#formdata) - [body.json()](#bodyjson) - [body.text()](#bodytext) - [Class: FetchError](#class-fetcherror) - [Class: AbortError](#class-aborterror) - [TypeScript](#typescript) - [Acknowledgement](#acknowledgement) - [Team](#team) - [Former](#former) - [License](#license) ## Motivation Instead of implementing `XMLHttpRequest` in Node.js to run browser-specific [Fetch polyfill](https://github.com/github/fetch), why not go from native `http` to `fetch` API directly? Hence, `node-fetch`, minimal code for a `window.fetch` compatible API on Node.js runtime. See Jason Miller's [isomorphic-unfetch](https://www.npmjs.com/package/isomorphic-unfetch) or Leonardo Quixada's [cross-fetch](https://github.com/lquixada/cross-fetch) for isomorphic usage (exports `node-fetch` for server-side, `whatwg-fetch` for client-side). ## Features - Stay consistent with `window.fetch` API. - Make conscious trade-off when following [WHATWG fetch spec][whatwg-fetch] and [stream spec](https://streams.spec.whatwg.org/) implementation details, document known differences. - Use native promise and async functions. - Use native Node streams for body, on both request and response. - Decode content encoding (gzip/deflate/brotli) properly, and convert string output (such as `res.text()` and `res.json()`) to UTF-8 automatically. - Useful extensions such as redirect limit, response size limit, [explicit errors][error-handling.md] for troubleshooting. ## Difference from client-side fetch - See known differences: - [As of v3.x](docs/v3-LIMITS.md) - [As of v2.x](docs/v2-LIMITS.md) - If you happen to use a missing feature that `window.fetch` offers, feel free to open an issue. - Pull requests are welcomed too! ## Installation Current stable release (`3.x`) requires at least Node.js 12.20.0. ```sh npm install node-fetch ``` ## Loading and configuring the module ### ES Modules (ESM) ```js import fetch from 'node-fetch'; ``` ### CommonJS `node-fetch` from v3 is an ESM-only module - you are not able to import it with `require()`. If you cannot switch to ESM, please use v2 which remains compatible with CommonJS. Critical bug fixes will continue to be published for v2. ```sh npm install node-fetch@2 ``` Alternatively, you can use the async `import()` function from CommonJS to load `node-fetch` asynchronously: ```js // mod.cjs const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); ``` ### Providing global access To use `fetch()` without importing it, you can patch the `global` object in node: ```js // fetch-polyfill.js import fetch, { Blob, blobFrom, blobFromSync, File, fileFrom, fileFromSync, FormData, Headers, Request, Response, } from 'node-fetch' if (!globalThis.fetch) { globalThis.fetch = fetch globalThis.Headers = Headers globalThis.Request = Request globalThis.Response = Response } // index.js import './fetch-polyfill' // ... ``` ## Upgrading Using an old version of node-fetch? Check out the following files: - [2.x to 3.x upgrade guide](docs/v3-UPGRADE-GUIDE.md) - [1.x to 2.x upgrade guide](docs/v2-UPGRADE-GUIDE.md) - [Changelog](https://github.com/node-fetch/node-fetch/releases) ## Common Usage NOTE: The documentation below is up-to-date with `3.x` releases, if you are using an older version, please check how to [upgrade](#upgrading). ### Plain text or HTML ```js import fetch from 'node-fetch'; const response = await fetch('https://github.com/'); const body = await response.text(); console.log(body); ``` ### JSON ```js import fetch from 'node-fetch'; const response = await fetch('https://api.github.com/users/github'); const data = await response.json(); console.log(data); ``` ### Simple Post ```js import fetch from 'node-fetch'; const response = await fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}); const data = await response.json(); console.log(data); ``` ### Post with JSON ```js import fetch from 'node-fetch'; const body = {a: 1}; const response = await fetch('https://httpbin.org/post', { method: 'post', body: JSON.stringify(body), headers: {'Content-Type': 'application/json'} }); const data = await response.json(); console.log(data); ``` ### Post with form parameters `URLSearchParams` is available on the global object in Node.js as of v10.0.0. See [official documentation](https://nodejs.org/api/url.html#url_class_urlsearchparams) for more usage methods. NOTE: The `Content-Type` header is only set automatically to `x-www-form-urlencoded` when an instance of `URLSearchParams` is given as such: ```js import fetch from 'node-fetch'; const params = new URLSearchParams(); params.append('a', 1); const response = await fetch('https://httpbin.org/post', {method: 'POST', body: params}); const data = await response.json(); console.log(data); ``` ### Handling exceptions NOTE: 3xx-5xx responses are _NOT_ exceptions, and should be handled in `then()`, see the next section. Wrapping the fetch function into a `try/catch` block will catch _all_ exceptions, such as errors originating from node core libraries, like network errors, and operational errors which are instances of FetchError. See the [error handling document][error-handling.md] for more details. ```js import fetch from 'node-fetch'; try { await fetch('https://domain.invalid/'); } catch (error) { console.log(error); } ``` ### Handling client and server errors It is common to create a helper function to check that the response contains no client (4xx) or server (5xx) error responses: ```js import fetch from 'node-fetch'; class HTTPResponseError extends Error { constructor(response) { super(`HTTP Error Response: ${response.status} ${response.statusText}`); this.response = response; } } const checkStatus = response => { if (response.ok) { // response.status >= 200 && response.status < 300 return response; } else { throw new HTTPResponseError(response); } } const response = await fetch('https://httpbin.org/status/400'); try { checkStatus(response); } catch (error) { console.error(error); const errorBody = await error.response.text(); console.error(`Error body: ${errorBody}`); } ``` ### Handling cookies Cookies are not stored by default. However, cookies can be extracted and passed by manipulating request and response headers. See [Extract Set-Cookie Header](#extract-set-cookie-header) for details. ## Advanced Usage ### Streams The "Node.js way" is to use streams when possible. You can pipe `res.body` to another stream. This example uses [stream.pipeline](https://nodejs.org/api/stream.html#stream_stream_pipeline_streams_callback) to attach stream error handlers and wait for the download to complete. ```js import {createWriteStream} from 'node:fs'; import {pipeline} from 'node:stream'; import {promisify} from 'node:util' import fetch from 'node-fetch'; const streamPipeline = promisify(pipeline); const response = await fetch('https://github.githubassets.com/images/modules/logos_page/Octocat.png'); if (!response.ok) throw new Error(`unexpected response ${response.statusText}`); await streamPipeline(response.body, createWriteStream('./octocat.png')); ``` In Node.js 14 you can also use async iterators to read `body`; however, be careful to catch errors -- the longer a response runs, the more likely it is to encounter an error. ```js import fetch from 'node-fetch'; const response = await fetch('https://httpbin.org/stream/3'); try { for await (const chunk of response.body) { console.dir(JSON.parse(chunk.toString())); } } catch (err) { console.error(err.stack); } ``` In Node.js 12 you can also use async iterators to read `body`; however, async iterators with streams did not mature until Node.js 14, so you need to do some extra work to ensure you handle errors directly from the stream and wait on it response to fully close. ```js import fetch from 'node-fetch'; const read = async body => { let error; body.on('error', err => { error = err; }); for await (const chunk of body) { console.dir(JSON.parse(chunk.toString())); } return new Promise((resolve, reject) => { body.on('close', () => { error ? reject(error) : resolve(); }); }); }; try { const response = await fetch('https://httpbin.org/stream/3'); await read(response.body); } catch (err) { console.error(err.stack); } ``` ### Accessing Headers and other Metadata ```js import fetch from 'node-fetch'; const response = await fetch('https://github.com/'); console.log(response.ok); console.log(response.status); console.log(response.statusText); console.log(response.headers.raw()); console.log(response.headers.get('content-type')); ``` ### Extract Set-Cookie Header Unlike browsers, you can access raw `Set-Cookie` headers manually using `Headers.raw()`. This is a `node-fetch` only API. ```js import fetch from 'node-fetch'; const response = await fetch('https://example.com'); // Returns an array of values, instead of a string of comma-separated values console.log(response.headers.raw()['set-cookie']); ``` ### Post data using a file ```js import fetch, { Blob, blobFrom, blobFromSync, File, fileFrom, fileFromSync, } from 'node-fetch' const mimetype = 'text/plain' const blob = fileFromSync('./input.txt', mimetype) const url = 'https://httpbin.org/post' const response = await fetch(url, { method: 'POST', body: blob }) const data = await response.json() console.log(data) ``` node-fetch comes with a spec-compliant [FormData] implementations for posting multipart/form-data payloads ```js import fetch, { FormData, File, fileFrom } from 'node-fetch' const httpbin = 'https://httpbin.org/post' const formData = new FormData() const binary = new Uint8Array([ 97, 98, 99 ]) const abc = new File([binary], 'abc.txt', { type: 'text/plain' }) formData.set('greeting', 'Hello, world!') formData.set('file-upload', abc, 'new name.txt') const response = await fetch(httpbin, { method: 'POST', body: formData }) const data = await response.json() console.log(data) ``` If you for some reason need to post a stream coming from any arbitrary place, then you can append a [Blob] or a [File] look-a-like item. The minimum requirement is that it has: 1. A `Symbol.toStringTag` getter or property that is either `Blob` or `File` 2. A known size. 3. And either a `stream()` method or a `arrayBuffer()` method that returns a ArrayBuffer. The `stream()` must return any async iterable object as long as it yields Uint8Array (or Buffer) so Node.Readable streams and whatwg streams works just fine. ```js formData.append('upload', { [Symbol.toStringTag]: 'Blob', size: 3, *stream() { yield new Uint8Array([97, 98, 99]) }, arrayBuffer() { return new Uint8Array([97, 98, 99]).buffer } }, 'abc.txt') ``` ### Request cancellation with AbortSignal You may cancel requests with `AbortController`. A suggested implementation is [`abort-controller`](https://www.npmjs.com/package/abort-controller). An example of timing out a request after 150ms could be achieved as the following: ```js import fetch, { AbortError } from 'node-fetch'; // AbortController was added in node v14.17.0 globally const AbortController = globalThis.AbortController || await import('abort-controller') const controller = new AbortController(); const timeout = setTimeout(() => { controller.abort(); }, 150); try { const response = await fetch('https://example.com', {signal: controller.signal}); const data = await response.json(); } catch (error) { if (error instanceof AbortError) { console.log('request was aborted'); } } finally { clearTimeout(timeout); } ``` See [test cases](https://github.com/node-fetch/node-fetch/blob/master/test/) for more examples. ## API ### fetch(url[, options]) - `url` A string representing the URL for fetching - `options` [Options](#fetch-options) for the HTTP(S) request - Returns: Promise<[Response](#class-response)> Perform an HTTP(S) fetch. `url` should be an absolute URL, such as `https://example.com/`. A path-relative URL (`/file/under/root`) or protocol-relative URL (`//can-be-http-or-https.com/`) will result in a rejected `Promise`. ### Options The default values are shown after each option key. ```js { // These properties are part of the Fetch Standard method: 'GET', headers: {}, // Request headers. format is the identical to that accepted by the Headers constructor (see below) body: null, // Request body. can be null, or a Node.js Readable stream redirect: 'follow', // Set to `manual` to extract redirect headers, `error` to reject redirect signal: null, // Pass an instance of AbortSignal to optionally abort requests // The following properties are node-fetch extensions follow: 20, // maximum redirect count. 0 to not follow redirect compress: true, // support gzip/deflate content encoding. false to disable size: 0, // maximum response body size in bytes. 0 to disable agent: null, // http(s).Agent instance or function that returns an instance (see below) highWaterMark: 16384, // the maximum number of bytes to store in the internal buffer before ceasing to read from the underlying resource. insecureHTTPParser: false // Use an insecure HTTP parser that accepts invalid HTTP headers when `true`. } ``` #### Default Headers If no values are set, the following request headers will be sent automatically: | Header | Value | | ------------------- | ------------------------------------------------------ | | `Accept-Encoding` | `gzip, deflate, br` (when `options.compress === true`) | | `Accept` | `*/*` | | `Content-Length` | _(automatically calculated, if possible)_ | | `Host` | _(host and port information from the target URI)_ | | `Transfer-Encoding` | `chunked` _(when `req.body` is a stream)_ | | `User-Agent` | `node-fetch` | Note: when `body` is a `Stream`, `Content-Length` is not set automatically. #### Custom Agent The `agent` option allows you to specify networking related options which are out of the scope of Fetch, including and not limited to the following: - Support self-signed certificate - Use only IPv4 or IPv6 - Custom DNS Lookup See [`http.Agent`](https://nodejs.org/api/http.html#http_new_agent_options) for more information. If no agent is specified, the default agent provided by Node.js is used. Note that [this changed in Node.js 19](https://github.com/nodejs/node/blob/4267b92604ad78584244488e7f7508a690cb80d0/lib/_http_agent.js#L564) to have `keepalive` true by default. If you wish to enable `keepalive` in an earlier version of Node.js, you can override the agent as per the following code sample. In addition, the `agent` option accepts a function that returns `http`(s)`.Agent` instance given current [URL](https://nodejs.org/api/url.html), this is useful during a redirection chain across HTTP and HTTPS protocol. ```js import http from 'node:http'; import https from 'node:https'; const httpAgent = new http.Agent({ keepAlive: true }); const httpsAgent = new https.Agent({ keepAlive: true }); const options = { agent: function(_parsedURL) { if (_parsedURL.protocol == 'http:') { return httpAgent; } else { return httpsAgent; } } }; ``` #### Custom highWaterMark Stream on Node.js have a smaller internal buffer size (16kB, aka `highWaterMark`) from client-side browsers (>1MB, not consistent across browsers). Because of that, when you are writing an isomorphic app and using `res.clone()`, it will hang with large response in Node. The recommended way to fix this problem is to resolve cloned response in parallel: ```js import fetch from 'node-fetch'; const response = await fetch('https://example.com'); const r1 = response.clone(); const results = await Promise.all([response.json(), r1.text()]); console.log(results[0]); console.log(results[1]); ``` If for some reason you don't like the solution above, since `3.x` you are able to modify the `highWaterMark` option: ```js import fetch from 'node-fetch'; const response = await fetch('https://example.com', { // About 1MB highWaterMark: 1024 * 1024 }); const result = await res.clone().arrayBuffer(); console.dir(result); ``` #### Insecure HTTP Parser Passed through to the `insecureHTTPParser` option on http(s).request. See [`http.request`](https://nodejs.org/api/http.html#http_http_request_url_options_callback) for more information. #### Manual Redirect The `redirect: 'manual'` option for node-fetch is different from the browser & specification, which results in an [opaque-redirect filtered response](https://fetch.spec.whatwg.org/#concept-filtered-response-opaque-redirect). node-fetch gives you the typical [basic filtered response](https://fetch.spec.whatwg.org/#concept-filtered-response-basic) instead. ```js import fetch from 'node-fetch'; const response = await fetch('https://httpbin.org/status/301', { redirect: 'manual' }); if (response.status === 301 || response.status === 302) { const locationURL = new URL(response.headers.get('location'), response.url); const response2 = await fetch(locationURL, { redirect: 'manual' }); console.dir(response2); } ``` ### Class: Request An HTTP(S) request containing information about URL, method, headers, and the body. This class implements the [Body](#iface-body) interface. Due to the nature of Node.js, the following properties are not implemented at this moment: - `type` - `destination` - `mode` - `credentials` - `cache` - `integrity` - `keepalive` The following node-fetch extension properties are provided: - `follow` - `compress` - `counter` - `agent` - `highWaterMark` See [options](#fetch-options) for exact meaning of these extensions. #### new Request(input[, options]) _(spec-compliant)_ - `input` A string representing a URL, or another `Request` (which will be cloned) - `options` [Options](#fetch-options) for the HTTP(S) request Constructs a new `Request` object. The constructor is identical to that in the [browser](https://developer.mozilla.org/en-US/docs/Web/API/Request/Request). In most cases, directly `fetch(url, options)` is simpler than creating a `Request` object. ### Class: Response An HTTP(S) response. This class implements the [Body](#iface-body) interface. The following properties are not implemented in node-fetch at this moment: - `trailer` #### new Response([body[, options]]) _(spec-compliant)_ - `body` A `String` or [`Readable` stream][node-readable] - `options` A [`ResponseInit`][response-init] options dictionary Constructs a new `Response` object. The constructor is identical to that in the [browser](https://developer.mozilla.org/en-US/docs/Web/API/Response/Response). Because Node.js does not implement service workers (for which this class was designed), one rarely has to construct a `Response` directly. #### response.ok _(spec-compliant)_ Convenience property representing if the request ended normally. Will evaluate to true if the response status was greater than or equal to 200 but smaller than 300. #### response.redirected _(spec-compliant)_ Convenience property representing if the request has been redirected at least once. Will evaluate to true if the internal redirect counter is greater than 0. #### response.type _(deviation from spec)_ Convenience property representing the response's type. node-fetch only supports `'default'` and `'error'` and does not make use of [filtered responses](https://fetch.spec.whatwg.org/#concept-filtered-response). ### Class: Headers This class allows manipulating and iterating over a set of HTTP headers. All methods specified in the [Fetch Standard][whatwg-fetch] are implemented. #### new Headers([init]) _(spec-compliant)_ - `init` Optional argument to pre-fill the `Headers` object Construct a new `Headers` object. `init` can be either `null`, a `Headers` object, an key-value map object or any iterable object. ```js // Example adapted from https://fetch.spec.whatwg.org/#example-headers-class import {Headers} from 'node-fetch'; const meta = { 'Content-Type': 'text/xml' }; const headers = new Headers(meta); // The above is equivalent to const meta = [['Content-Type', 'text/xml']]; const headers = new Headers(meta); // You can in fact use any iterable objects, like a Map or even another Headers const meta = new Map(); meta.set('Content-Type', 'text/xml'); const headers = new Headers(meta); const copyOfHeaders = new Headers(headers); ``` ### Interface: Body `Body` is an abstract interface with methods that are applicable to both `Request` and `Response` classes. #### body.body _(deviation from spec)_ - Node.js [`Readable` stream][node-readable] Data are encapsulated in the `Body` object. Note that while the [Fetch Standard][whatwg-fetch] requires the property to always be a WHATWG `ReadableStream`, in node-fetch it is a Node.js [`Readable` stream][node-readable]. #### body.bodyUsed _(spec-compliant)_ - `Boolean` A boolean property for if this body has been consumed. Per the specs, a consumed body cannot be used again. #### body.arrayBuffer() #### body.formData() #### body.blob() #### body.json() #### body.text() `fetch` comes with methods to parse `multipart/form-data` payloads as well as `x-www-form-urlencoded` bodies using `.formData()` this comes from the idea that Service Worker can intercept such messages before it's sent to the server to alter them. This is useful for anybody building a server so you can use it to parse & consume payloads.
Code example ```js import http from 'node:http' import { Response } from 'node-fetch' http.createServer(async function (req, res) { const formData = await new Response(req, { headers: req.headers // Pass along the boundary value }).formData() const allFields = [...formData] const file = formData.get('uploaded-files') const arrayBuffer = await file.arrayBuffer() const text = await file.text() const whatwgReadableStream = file.stream() // other was to consume the request could be to do: const json = await new Response(req).json() const text = await new Response(req).text() const arrayBuffer = await new Response(req).arrayBuffer() const blob = await new Response(req, { headers: req.headers // So that `type` inherits `Content-Type` }.blob() }) ```
### Class: FetchError _(node-fetch extension)_ An operational error in the fetching process. See [ERROR-HANDLING.md][] for more info. ### Class: AbortError _(node-fetch extension)_ An Error thrown when the request is aborted in response to an `AbortSignal`'s `abort` event. It has a `name` property of `AbortError`. See [ERROR-HANDLING.MD][] for more info. ## TypeScript **Since `3.x` types are bundled with `node-fetch`, so you don't need to install any additional packages.** For older versions please use the type definitions from [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped): ```sh npm install --save-dev @types/node-fetch@2.x ``` ## Acknowledgement Thanks to [github/fetch](https://github.com/github/fetch) for providing a solid implementation reference. ## Team | [![David Frank](https://github.com/bitinn.png?size=100)](https://github.com/bitinn) | [![Jimmy Wärting](https://github.com/jimmywarting.png?size=100)](https://github.com/jimmywarting) | [![Antoni Kepinski](https://github.com/xxczaki.png?size=100)](https://github.com/xxczaki) | [![Richie Bendall](https://github.com/Richienb.png?size=100)](https://github.com/Richienb) | [![Gregor Martynus](https://github.com/gr2m.png?size=100)](https://github.com/gr2m) | | ----------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------- | | [David Frank](https://bitinn.net/) | [Jimmy Wärting](https://jimmy.warting.se/) | [Antoni Kepinski](https://kepinski.ch) | [Richie Bendall](https://www.richie-bendall.ml/) | [Gregor Martynus](https://twitter.com/gr2m) | ###### Former - [Timothy Gu](https://github.com/timothygu) - [Jared Kantrowitz](https://github.com/jkantr) ## License [MIT](LICENSE.md) [whatwg-fetch]: https://fetch.spec.whatwg.org/ [response-init]: https://fetch.spec.whatwg.org/#responseinit [node-readable]: https://nodejs.org/api/stream.html#stream_readable_streams [mdn-headers]: https://developer.mozilla.org/en-US/docs/Web/API/Headers [error-handling.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md [FormData]: https://developer.mozilla.org/en-US/docs/Web/API/FormData [Blob]: https://developer.mozilla.org/en-US/docs/Web/API/Blob [File]: https://developer.mozilla.org/en-US/docs/Web/API/File node-fetch-3.3.2/SECURITY.md000066400000000000000000000001441445773333000153340ustar00rootroot00000000000000# Security Policy ## Reporting a Vulnerability Please report security issues to `jimmy@warting.se`node-fetch-3.3.2/docs/000077500000000000000000000000001445773333000144745ustar00rootroot00000000000000node-fetch-3.3.2/docs/ERROR-HANDLING.md000066400000000000000000000034661445773333000171020ustar00rootroot00000000000000 Error handling with node-fetch ============================== Because `window.fetch` isn't designed to be transparent about the cause of request errors, we have to come up with our own solutions. The basics: - A cancelled request is rejected with an [`AbortError`](https://github.com/node-fetch/node-fetch/blob/master/README.md#class-aborterror). You can check if the reason for rejection was that the request was aborted by checking the `Error`'s `name` is `AbortError`. ```js const fetch = require('node-fetch'); (async () => { try { await fetch(url, {signal}); } catch (error) { if (error.name === 'AbortError') { console.log('request was aborted'); } } })(); ``` - All [operational errors][joyent-guide] *other than aborted requests* are rejected with a [FetchError](https://github.com/node-fetch/node-fetch/blob/master/README.md#class-fetcherror). You can handle them all through the `try/catch` block or promise `catch` clause. - All errors come with an `error.message` detailing the cause of errors. - All errors originating from `node-fetch` are marked with a custom `err.type`. - All errors originating from Node.js core are marked with `error.type = 'system'`, and in addition contain an `error.code` and an `error.errno` for error handling. These are aliases for error codes thrown by Node.js core. - [Programmer errors][joyent-guide] are either thrown as soon as possible, or rejected with default `Error` with `error.message` for ease of troubleshooting. List of error types: - Because we maintain 100% coverage, see [test/main.js](https://github.com/node-fetch/node-fetch/blob/master/test/main.js) for a full list of custom `FetchError` types, as well as some of the common errors from Node.js [joyent-guide]: https://www.joyent.com/node-js/production/design/errors#operational-errors-vs-programmer-errors node-fetch-3.3.2/docs/media/000077500000000000000000000000001445773333000155535ustar00rootroot00000000000000node-fetch-3.3.2/docs/media/Banner.svg000066400000000000000000000074241445773333000175100ustar00rootroot00000000000000 Created with Lunacy node-fetch-3.3.2/docs/media/Logo.svg000066400000000000000000000024311445773333000171740ustar00rootroot00000000000000 Created with Lunacy node-fetch-3.3.2/docs/media/NodeFetch.sketch000066400000000000000000001004011445773333000206110ustar00rootroot00000000000000PK€|$PP+A^¿ user.jsonmͱn„@ Ð_9¹Æh½¶w×t, J)]úS„ ÑÝ¥9Ä¿IeÚÑÌÛ {f$Š‚Ò–„Ys‹A)çà3s,ÐlðužÆçùz{çéý Ò^A)”¬GêbBésÄD!¢¹”Ìzו6üŒ¯o—uY^.ó4B‹Ö$D”©:±q­BŽ4xÝ¡‚ûº~¼ž—ïW »ßDR¯Ù¨ žX±/¾;ä"hGƒidCÛ‘ùòŸìª“ûsOûþPK€|$Pé–, k1 document.json­’]Oƒ0…ÿ ×k¤¥¬­w-…dƯ8ï̲0ÖML¡¦c‰ËÂ÷™VñÆ;8§ç<'Ðc´¬l¹ÛEçÑÚUûÆ´]4Ç¥[=™ª›i0I’„bŠ0fQ©9R©’hšb¥¦D% ÓªöÞCü¶ÜšY»6/Ñy<‰ ÚtÐ~üä R欅þÚµ?p„ÁÂPÎb‚(•ÉBK¤yL(UD2E T7‚¾‡ u^ލ7aëËu cBmÜpÓwï³þØ÷cÿ·êá¼ó¦Þ¶—åÁøyw°§M£>?4+g¿j÷æ¥ Ú0¬Ú=–Þ¬#smWÖ­ñ0íý[øïá%LÚÁpÊò‡ù{Éóø‚ìÕüb~s]ÔÖÜ™+Qˆ-½ÙGfM³ïÊ•5o×e´ÁêÎ8¥s‘#œ1Žh®âxʈ9"3-§Q?ùW¦Àº 8IQ®ILM‘HS”‹BfXõ‹þPK€|$PÉB)½%/pages/844D189E-1C78-4EB7-8167-908899E0CDA6.jsoníXmoã6þ+î«eˆzg¾I²t›»l³hÒí‡vQÐcó"‹E5qÿ÷вlÙÞë6ûr[# Ø3Ô¼ÏðÑ<¿å%iãҨɂ£à¿ñùh.¯f@ ]w†Bœš( BÓMãÀ ‘˜Ø CŒS+™E><4缤¤º©© ’ñʸ4ÑÄ O5ò¦VÐð¼Ó5ælOf\¬ˆþ/&«ò²-hqMÖT\=µT߉ÖÄh–¼-‹;ÁVÆå=)º™÷‚¬èHŸ@MOIAXõNp¥PKé›KÊKi\:ˆ}d…\öŸŸ:Ekø ²Y“±'ZÜñ÷Œ>*ÃóÀ)Y]Óâ ì^IRóÞSК8×< Åá=kؼ¤hiïò5kdúT“ "r·®igRÕ¹i¼ƒÄ] C¿Òö âmجZ$[×Á`ßÙÑa‚Ë>uð¥m æÕ=WAÌùjÊTäÂiÙV$_+¢*–„—\€þ_¤‰úýÕ06›mJbAÉÃ[Ò<$KÐ:ØÓÈu9Φ@ TÅŒæ\Ð`ÖŠIå=ü3.‘Ê·$Bž8×Ñßñ@Å@‰”GVàô­²­KÒ$%Ëî–‚·‹å6æ}ñ]UIÉÛ⧺ä¤åC•ãž {Ή(Žú'È\œà›Þ,‰L7@®‰86C+±,„-/ô¬3ëÙCÿtŸ¿uÿd%|Õ±¥ì¡¥®ù‚6”~ôeýt-qªì%}’G%ŸÆ¾“ú6”d¦ë{©93dÚ©#ÏvwfŸ,yë<*M1üøØ m×Ö徿ë µþ†’¢dÕ‰+äãoÿ«lçe+FŒŽ lK+¦í¬Íiµ 'ž­©7¹€?8·â*ÎQµ(ûV"kAŽJ‰l·õ.H]Å›.Qªo©”`ë¸v‚ÔK–7ÉÁe©j^h=¼&9“kÝŸ×{ž£í¨J~z‹HJÁæ­¤Éoo‡ïÅ-|¨Ø<ÐFÂî3£M.X-¹ /‰‚¤ÁyÇÛá„h ù»̘—EwW+ìnõÓóîÂEe½$àæ4 à mdz±‹PØe^û¿”ê–*Ý*CƒóÛ"ŽJ¶¨VPÿ¦ë.›5¤ËÕqäXʆþa(FåÈï‡Bu›%)øã0 **n÷)Ÿ6"ç\{3d¿°GœÃIh(Àa:3“`fÁ$t=G(2ãÈ Œƒ4μ“m¡Ú1!uKþÅYµ¥¨‘Åʲ÷B[ÑÏó‰Nšª|ÁËæ8ç”s¡\E=9U ÒÈNî²Ïô¨#U&>ã&$µ82÷ˆ©Ò§O?dõÿËh,©S»’†pò|gŒ€…šÜè°ƒ^›ñe͸™|bðÑùê '°=Ø÷­ Ü%cꇾ¸ž…CËÁ (èäLmM@ž>ðµ“õVrÀQJt¹ž òxSýPA”kÃ;¢´…<8¬k «öè¤àv]å€+ÖП™\Þ®Ws¾C*‹r]/cÞV ˜ÏÏŽ?µlc'ľºîäÂõàúŸ\<»7ŋގ€ß–‚r]i#&æ­º¾«EL—äwƱuìö)5/Iþ°Ê”þöã™EÛL¢!s¨ÏT•ãCa5-§&±˜IÆør *{š¾Q[VtÓK%¥‡³ò¯ªôIãÆ?SÝÞ‰/µ ›äTæj‡T‚JÒñ}¡8·]·A¡K€\ÿÊ[)‘·ÝT`«º¤Šil:ˆÊ[yZ$ä£]U?kp탅”ç>D֮ީ×P ;˜®xGÀyÑJ@roz¯¼¦47­lXAöØÖ~foîï*õ*¦]ÍÜ'¹–(à=´Sð¶-%«á¥ª|d%\Še/»Ö^lt4tlF£ô£Kßñ}Ìff䯩éf121Ž<ÓMÒp–aË£èÌ– ½[Ò!;Ô[:Ïñÿf{†˜(HúºixÙ¦ÁuN³À@ùP÷85C/@¦3³ý HB'HÜóÚ4Ø(Êc¬— HW½íÙßù‚Á~]0œÝ‚Á¶¾¬FßFôªºJAµœÝ>"±³ÈÍ\dÚQ8Ásl3ŽclFNÅY’Eaðºø2û輋ŒÊ|ùù{ ÷«¿›~áâ‹õä'¯%Ü]ìý3‰ýßm~þß¶öÔñ,Ëò‘åxàº3¹0‘­–ûSX6€N×R ÛÆ¯+‰×•ĉ•ÄÞÞáÓv:6ßÕJÂÿèJâר¤Ï­ƒ/œðÍPK€|$Pô$B//pages/91DF2135-ED2C-4ED4-9557-8F489FAC192D.jsonSÛnÚ@ý—í«‰b.-ø­±¡E"JÔP^š(Z¼ƒ=e½kí® &âß3kƒ)mŸ*K¾œÙ3sfæø½¦’[Ë"Vò XÀ„~Õ럺yBà$Lfýp0êM“~ÜN“ao2}êgÃñdö9'ý„Hk­%põP‚áµbQ/ ìKmÜCéªðv©u9ŸœiSpGñ/C•ÊJ€XðÌ\œP鿺Œ·³¹®¤X,X´áÒÂ1`à ¸ªg¨*“ËŽêÑh_°ÍÒЖf¹cÑà–ÒîP¸üô¾o Õt§Ühg¸±Ô+„ÏÑñ)"±,A|ÕZ9.ÿŽ­€ª¦W‘…N· ~Vhq-¡CšžhÝt_rE#YÖ%4šTÓ'{ª‹µ–~Ž˜· ;º‹TY|nž$\ð.›Ñî´<ú¨,M]m´cª‹ô³ßÈJñ´ö ·K¬¥6$à™}˜ÆþzfÌwpÏ­ÓÊzô¶Š˜3°ãñ¼°;|KǶqNŠ:­ÖÕòzs-BQ"T·öê$èühèÁ¢Ð»Áqãþq®Áï¹Ù‚é0Êø²C%h ß*¯!$­9·±Ät»Ì®2òƒï¢³æ\ÅRWâ{)5çX³¬“YóÎ>¥I¸ã×®ìPúƒ¸mEd h€_'»ü/?3(<íTqòÔÅ>ò„âŒærjò !K,<—þÝ˰(%ø ­ïPK€|$PÛ¤àÅuqõ‰previews/preview.pngì»…WU[÷? ˆŠ„”Ò! ¤„¤Ò%]ÒÒ’ÒJ7(J—€ˆ”HwwK·pèîŽßÚ ÷ûþïã¸c\8gïµ×škÎO̵ ”“CC&@†ƒƒC“V€ƒ»‡Ð‡t|’f’ þ‡h+(¡„þ¹!ÀÁ=“~¥ä”°>å3lŸxµsäÞûCXîƒÈžœé¡"›b§¦ãD£r®Å[£¼7è%SæáߊàþPaá ~Kà§^BB'ÅÉÖ âIžméíd>Á†Ú ¿è4S+À}>©tŽ`;“|–äÌ›—-@´yâ¡ýœÂ¦Û¶f¦ýqp³Ÿ$ctç—ÝÓKx05¸)Uþ¸›).H!7?½4B ¸ù)"¶éæ'*f„›Ÿbüï²ÿ]ö¿ËþwÙÿ/†ÝÑ[}pó»»©"º¸áåùÉNüÌ9UP³Þ¼M9×rŠ÷\¶¬4ÊÍUtêyj 8Šá4¥‹‹‹xì¦J¼'óŸû+·–——NOO[HpuܬQIx¹„áh²4£é\çf˜ ûS2—#™-Ç‹hµÝvšip‚ ;¿˜˜™ß'qzÂa5M¤ªíœÑÖæ`>š°¬|¦®¡‘æyu™µT•ö:ž£ùo½7ìî~8F}XbÓÙáäÒì)jQ#vY úàqïî¡&‰³K‘Á…5…ÞkjÏkõ ô¨ó‚/pØ笾X¸rÎQ+ ·¬½8Äãz¯ûÑP)»•Ô“7¯bõç [ÉÄÉR2)¬%TŠO?S«ð︉©ó^í@Ÿ²:·lþÿ—{×1µ9¼£06üy–€&¥sªÀ Yö+$ø‡ƒ½ý£±"ctðááá [ESÝt ™×ŸUÕÎDèN¯ >ý;0çË“%Ø\0SØÌù"gV¦åêêj ~½<»¥Ï‰–Dàм©‚ìuØ !À8ª¶PXôšœf}qd¯WÆzÂ\ãýýûw®½.6´°UÚ;PóJQQQÓaÌpg»=3é_VH<ÁÏ}SHÕ#puî4)ößPˆ-}ccøK‰ŽÚn[®3R|woîø„Wªhll,ÈÍ^s6µõýv(CjæM¯ááá²¹ z½Ãõ±¬ÌùöæAÏÛÝùvûÕÁåõuéÛ‘:u'%J§4¤“x_ Þ|Hƒò¡¥+–Ú”ÄÂÂâ¿QÀLµæUkO•CHø¸ãââþ›é'L¼Ê>&`ZJrµ<ŸÃ­¿]žC\Ú=©¯ °©šôÃù¾Þ>}ýчi•|mâ4ÑРäóyêÛ§×=ÄÆëü‘–FÁX>ÿ)æ…­ÑíöýBlaøm(嶦1ijUÅÆÍãØûojäÌT}ÃÃ8Ñ:ΓïÑäAMÞ~×…Øâ“W{yZv¾Û9Õ~Sí/‡~¼æK¡¦„Ìbܓж´gÝ/‚+’Ìï® {³•^6÷õõšõXŒz“ˆ£5m$®£Jt3—Qª‚`Òòi¢Íß_Çÿ·¥”Ì*PéôðMªU»ØßNû+b òéÕ)”±jŦÊÿ}C… W½ìý[TB†¯ o>ÝÆèœ AßW½Âî]‚u þÎjvþiæIάò«9·õ]f"SÕ¶àíX˜úÍ•n³(:Љ¨nŽ ¯ ÙåIòÕ Ëò…¯ÿÝôܤ̀ËËRÌh¶únJ[Ê_Àì:väänu[”-Û(™ø&GÓTf ûf5S“E o§aá[2ˆ}=ÝǺQ´I³ó±ße—§ki%Ãþ~}¨$ƸÆÅì°Uì:o[>h{ìý5lØ„¾¨Yâ,!æ&¦/EäÒ*ÅHœŸö🭗­þÌÖëå;³•)okÜâ' ›8ý7gÝøsÔKƒé*gë1ÃèæO”R`—K…^ ¡®ð<ýW«Nbúœ{Ëiá- B„„æ õGžh2ò>z|Ó'ÌB(¸Œ!½‡…&ýß½rlÊ‘1¯¿¦_+æWªkÚõ‹ìLÚöfüÛã—œäÌúÁ8Ï‚úÓÄZ;™jnÓbʧn´û™N ¯!Níq3Ómò¥bà¹zuÇs¶fDk óíËÝuµB£×!¤ü—3W|·iñ3¦nÔ»¨¤¤å‡|ÚÜá„•ì¿Êõ¢£d>|9ðýuðZ^%ã-0ýŒ¨½cïàЂÏ6·o%óßÕ̇ºªZöÊÒ±¬”P]ÞNI)„oÒ±f/1†×Ùæ²ïm>ü@@ €*ël½0 Tûí|èêÄ……»y8{^,%˜˜šJÞ>™Šy—Úv©'…©Š9.>þ¿G>9gÚ[ìöRƒ³±±q¯ˆÈ:q…«C«+ÅL™P4BŽôÛåßC hïNâãa¬=ß5y÷Nâ6/Ÿ0+`zì§‘6ú%^νU^xYx²¼;š@ììTÎ\0Þ.fÚ¯N\$–ÇÑB—ƒÕÛå;¡ P<^ZÉL¾¦~mç…/·ß³oVkÏ´ ‘䲚õß=w>DŒn¹‹«‰±·Ôû€ ˜tF3…Ï6ú·R„óþÇRdÌ*Uðwî5Áß'ŠpUÐ(µ eÑoi‚jþvTÞ S3ZÐÝCÅ‹p‘T‚ÄPÐ ·›¿Ü8’óýß:®ÕisS­ì"£X˜J:£Ð¨û¶‚SZ±ðè'¿>Óø”°õ êf\€„ƒ¶t #µ—1,ú¢«ø7ÏæûT§6 ¤yòÕ…ÿÊ')ÆÀϲü/v¾í=¯é8eo}0¤z«û`H!-§*ìû''‰01«]‚h¿7œŠtR6,[ù¯7ר?6ûoñ/ÃÁêËä4›]í÷ºŸ†‚ò„tVÞO"0¼ªtÜó,rr}--­[_AI îyL¯Ò´:”}52Á¹~æbõ0QÓñL±7`||¼mÁ&Ëøæú¨(° Ü°»8 LÔžå·üzÛˆÅðSâë<ruuõÿb8 ˆö—ÍBgu®j¾6ËÛ®ÃQ}lpuíù“>@— ëž. S½À½‰ftéÐÏR·3/ø;²‡y£ó—uÓ¤…ód&{¸{ðmÝø ÿÆ@½”}¬ò,¼z#ÿ`kF ¯úpÕ@W¹D$§9Ö8 pÝZù?ŸÕ‡¢¬{qz0q~pTsTÍâ~ºß¶äÎuµ<¤ªs-4?ÞEép0Ê×®&îü²Á{“ueÑmË|`!ŸÕ‡BbµÝAŸß´ÜŸÕKÓ錆†føâF~<¡dÿ‰œ£UÉ1°&Ãw0X¶ÿGJªê{EŽv57¤¤y<;ysppÄ¡”ÇÛ…H•cba*˜±R¬Ä+ EçX0W9Z9[¥Ù0Z ‚t‰Þöy ?l7›¢€—ýÒÕÕP§‘‹…»»;ĬüÛУœ”íßÔ¸9kº¬d죵¢Éˆ JYÉóDJ Å^Ÿùœ“ÓØãŠ÷W Ë@Çê5-JMewóQŒ4±pƱÄvòªªªŸÌn@[ ùC2„Zök%kî‡ãfùeÇsÁiŽ[Ó”êʉЪšÖ"äúüÉá~~~ŒµŒÿˆùïììGgÉAùn ô”jJS*yš×Ü@ìö[*?¾s\Z.¯o¢/I6¹§§¨Õ¡‡Õ[£úúûËýZU?NsujùÀFÁaXþm¶¸w‚Ǭû2Z ý&°€¤Lîô`ÍñøèÓ†ã.cþßà `ÌÊÖwήÜümv¸6’ß™wSÒr åpô7Æ‹ûÇÇ+Gb¸íMÝæ°“—”§"cÁZÃÃõ ÊF:l €.q¨§»™hPÝS1èUóBÀ|‰kDAé Z7aŠÙÍþÞ|;ÛàËâv¼=Zå¼'£EÝYîyS.‡ÐSiéuDZó<Æ£ èÛ¬Ü C÷ÏO9áT5q¾›]™Ž¬S£ÎÕ(Yú-5•,¯|1ŽkýWc…®òÑnŸzsS 6ˆ‚m¹æzɄ妞‘C‘¨k*ß ²8ë~D7`•o@Iå±´E@¾'Á¦ƒ¶@¯9ˆ±ÜÈÑzÆýèÚè›éœ?¬¾JrÝÄ)¥U””‚×\vÀY8ç~g†Ðû‹°3¥pß}í´åZ£sÑܘ¯Ÿª »ºŽÜõ¯>šæ½ÕÔI;ÑÙú½æpŸsÆÍvsЧ¸« @SP~TÞG\ÓàlºÒâ8!ÉTµÈ9¤ÿæ)tõá~KW ÃnŒ8ÄsÔ7„\Ûuw*÷+¦êìëõ$¼˜¸ðð¾îpŸB)v¸gXm¦‚ì&7s £y„¬å1EÆENâô÷·I´NeÜcdߘçïºâ9­*§­F´Cµ«]¾˜<–Ãjž`¼‘Ùæ`û'n™²Üà/ô<ëãYÿèùÓ¡õ?RL¡@ÐQí­€MJõ ¾)ã?XxÙÀ¿„ËþIËs«ú(ŒÐªŠ%ìÅb̧DùP eH¿Kåbè⮥•ø¯Udg@ƒvZê¦c-ÏbW,¶çI´ç›?¶?»m{ùpÁ&|«®¾ Ö*J6™ßPí&Ç#üêÔD$‰¡435Õ÷í´Ëzüž7Vþ ê¶,//;µŽ ÿCYG*f•HÇ>{_À`e âaóŸe;‡ô@IQ [sæ„5çüÅ…tsÈßl&­Ñ¸ñuÅ`6ú£3MÄý¢s±ª:áz»ch #Ä!VÏßýI‡2„»ë’E`µxn %ë:…­$®QÖ7µòÒÔõSÝ€.k½BNÆö±/?²²h©dðâ9,\:uH¹ÞëÆâÞL&*,VánK@À^ÓwéØOëç’lŽ›“e‹qfÓ»#TÙoöa MäââbúR²Çé ‹Ãúèü|ÙŠ4àˆøX\×9««2=þG‰þÐÐ÷ÁÐÔˆ¢Un €@ò*й:8ô¸8ú¦|sG@ˆÈkŒ‰™™`#Ùä×éÑV²‹¤ÙH^謵u±;þj¿ÜØÜ—*„w£ hˆ?¥'mS·ÙJþ®âx‘³DLÌ‚NÆÊ&m×/NoõÝ|8|¶B‚Ó\ ‹ÅxÞs`ÆÚ»cÙ¨¡ÂyÙ¹pîyó;  ÔÊ`aÁ@DÒOï%¥'}RY å"£&^wwwIß“n<„¸~ƒoËYöZÀ)œ™ôiò±ª&&ä‚þÝê.IAÝt)"—åTh \s ²«PcÇu«šÈò[Mó(~Û¿î·¬®»›”Yª:îìIn»íƒlÿŸÁ6&8¿Ë&“WðŸ4ÅúñšÏkÓøoÝIËëë!Pãò_«¤â¥X*r³ D^"(*Uì“„³o)@±7ÉL0¢iûúùóçMGµW×þám¾ý3 à[ ™àNÀÇÀ§­ÅpX¨ƒ £Ö‰à¼ˆÖ2®09¯Frû'§‰ìO*>%ȇèÙ—4ëlœEï|iì ÒZu mñr6«ý3uë?–½Úº• ´?;­dDKzô8/%âžmÕzJôÂÃjûê‘}å` µ ¤ ÿV\Xœ¾Týë°Ymâ·Š®ò’¶¾äOòm„š]ó6å×Ó.é´¬ÜPùjËa9‰÷ìÙ3Ã(pº®v¸¿©}kýVóå°çÐdÍœ~¹. ¶¼¢¢ª0ˆVÈoäÑË/ÀÅC¬¼ÓmåÁ¶ëœìpõ!ɕԙ Q1üî.ƒC_ïÃßðŽœ:07G+²žÜ–å 8‘7š‚ü äéµÞ(!¹8;‚ä;MÄÎLý–ÀäËýÌKó볩"zÊ÷å1 ˜)˜Éó¤ß—‡präPÇBâ3• šž;LW»L,ÖsMƒ…?§¡ ħ@¨Ò¬<¿»Ð ³M>ÓÜô©1œÜÔ×çL¡­ÑçÜ*VZÄÕ¦®™I€ôZí+›Äfì5‰xÿþ…Â6ý $1b{¥ë*‡Úbå6†«/à^>xÜ ðx@@6î9-d ã±Ü_¢Ÿý½ÊᲿüÎü^†+Dª\ÐW[oïKqaÏ{ƒçXí$»Š?¥"ôÚèùß”6È+yqî(}âÙï‚…ñlÜAç…¬}Š´Ç¶7i“1ÿ^ìh™µþ*­òZ„ÐÃÀ€±:… ¦äÞÖ2!¬+ø -Æy&{—‡A6ƒ\ËH‚‡zz¢t ®3gÓ1ìf*¨ÄÜϯ‡$ñ8žÅcцtçAÔO%EEů¶’ÎþOá~Y+s€çU:Ÿ}W)Äýhгæêb«5RŒ²rTç³è°ó½^\|¨2Ë+‚ êhîô‡{ÜŒ} ðæs€·oü™*ØJ/NH:﹬$€< Æ^¬Ç;ážD½üg擞W7`2è±±þÄ÷QP>‰¾=€êâóƒ=.ô¢+ø)yîxm¾ÒM'G#âb†,0$ÿt êW†7XP·öá>I$#Tæ?ðÖ4FZq=9ªÌ—¦pmŽæ$çö]€üZþó§ŠhtôTÓµg©ýµ©žæ+£q.2¥ªŸ;Å’Çù ͦÒjZ8_fÐáCŽ Ñ@lêï×íš—H!J©‘b- ç ã VmS¤æmO¸v¦¨›êï<¶8…\ËãdêvÅrÓPHiÝ Púù§:™¨tÚÅl½æl3+óeôü¬ð 7-`gƒâeÑĆΧn"Á—T'>Ä=ëƒ=G8s,ºdbVfLçÒndï ¾®F‰åP`Z!Ä<Ⱦ‚ X¡›¾æ)]®æ_ÒÕ€µ¥ ílGJF%DwÇ“Kü¸PæCœàE"¡›¾JlÓFf}4åØ )æ:õÁ/:¨„]‰×ÚçRˆÊÙF©Îuê_Õ \§þfíÕ%×FÑPW7ñ÷"ÑMŸà:þë ;œ° ®•ëæB0s®‹ý4 Oþ](¤›®1Jv_àø‘ã¤õu>î6’^2CnQ­ìýÛ1øX'^Z´DËkΩGÔ‰v àó—±¼ÀF[¿Ëlþ¯û²”šùÏ3Èÿõ$в"¼Dß Ô ÎpÝ„‹Š)khBá?o8º¨ øRóÑ_&ææ7ç@)Xx+Ã$<Ý_±î Í„EQí ¾(°œÂž H¸As]ÿÒÀé ô@¯Ð¹`³“ušÏâ´ý·l§…:óèЭÿx׺çÅRÀ=`Ø ¸×z‚[ÎåÐæ»Zwv2ß0Ô+ˆqQ°ÃáÑ×®l-p%Kªƒ& t"²×Í)‡@¬Eáÿ(pÄs:—6àÓ:™ñÌ™JØ9Eo(¨±¼ÃKþ…Äp²ÌÚºþ.®ðëׯ¡ãç·(NBü¯ÕV'³ª1Ãy:|ôx(/ ¼•P5dʆ»Mg Àí#j”˜B]ï®X¶‰²ŒóÇ%jÜ@âãÎ=T耉Æ:þL—ø<DŸ·ýÃÚäħÌôÝO’à Öœ 8ÉÝËVÍËΫ ã?È’®ý&m•¥tÓÅÎÎj¯Î ÖÎ7࣯3â—óÞ"t 8W4 \RRrÓý ¸*Üq½ µ…Øq¢îõ^N:θ[¿1Î3®³GYL×'à¸ÏŸÿ‘ÎÕé,ŪN8šÏ/l›Þe&.ZgAWMHèúÿ½/€WhÔÁ u¸m×sBÖß™k¾i#*¡}ÀœÖMv? äÙ,£¸<çÅ ƒ}>ÞÏ îüúLêú&…)dÛÔA-dÏ›uÎEn2÷hÒv§Ôó¬´€ï8¤KªC²z»Ü—j݈ÆÙ$Æ¿y=ìP v$þ¯H.LJ‡Vc³±› ‹Lä¡ I¢¡Bø…†@a.4Sã®ÿ']Âút%3ó:÷zHI/·ýaÓ®[¯o2üAÈöC »I!_ÅÖÃEq½lˆVô[ÃB!ý{S­AÌ‚¢P;àÁ5Ï4p`-¡Á÷Iݘÿ]C®)×d„R :¡»c^¬ÐáÔ=÷ypsd&Ç‚(:úýº,DpµžBÅZ`9UÁ a››˜¹t¸Ù¼Ç¯t}ˆ™mæÛÊNW³Ó¾§‚¦‡ ì=îR²§G‚ûá¿ݧÔÌ O»Œù±9§ô0½®ØW &Á^BRHü™õ”‚¹Aæ8¾ö$¾ì`X#M5_û&Ë€Í*3÷…1:¿Î²Oxo©ß7<°n%vlªûpç?µ,›õƒÎPþ;†ôúýš®Â[?ì€|ãD{óñ"Zˆ§›¸yx`€Ÿ÷^Ë^˜¾ò¾—üƒœ=72q¿¶IxI…þ7l Ÿ#0 üîøÜ;uÂ{”±PÒSbB_$²Äá‘.øšŸ\bòcG?º#·p?—8·_~Ýi œô€ÝH‡Ð8Ö«³{C’º>Œ»õèÌ›•[5P\õ:¾@1 T¡y4Ô~T;ƒŠ²ÍWrÁÒ‚ÖUb>†ŒËÂQ½Ñ;ѵ`Ù»”¦SëQ°UsÖ’òÊê©Ø”kÆâ:Zmll@Y©hÆÐæ`ÜÚ…VX8i0ôÞÔ ƒÞ[˜ß«¡QŸ˜ÐN¨Àf¡ãxÃÊó,L —H9ðÐÇ[@¬‘“C–|!æA”ðX¹Äúc‹ y†Z9£UäÚVÚXÍzÖñ[@K¬O»qÙ ‘“ÏUßyVWÑ&Ò{²e¹5>mȓ׼¬¼Ö _ýYïQšCËŸÅ  cˆÌJ8šw»;Ó² -[]Í”qZmêÒœÂï­wuy&è~gÓc+hÜÍ"ü41Z°cM}¤5.×l®¹˜©Œbµg,ÃhºÇn½íå­Wê7u}Û•ß%?Tev©wr=Τ¡z¹>t“6éiŸ¿Þåù Pƒ¶m?·¯å×’ à0ÜEÁi‚TûÊŸ yšGß@ØÕ54ôÛ?Qö–Œ@ît8;À@P´]Ž6‰‹‹‹­åÅšûÅH‰Åˆ¬…‰_÷÷ý­(T¯v±çÅb5]#àéå77÷·ÜÜg_™t.KÂ)§Ð„j®¦†ÿ*†‰³ä…w"ÃÝš´Z ‡ÇÆðéUr íà" (2°=Åîˆ ©(è@ ¸¬RçDAH@) ËÒÀ§gŽëîó_Kä<Yp4í½âxw=àjì¸ÒÉ¢Ñlgn<VºæIH_š²xØ-Ï€“†áK#Z€×¼®SÝ»dì6o[>ša7iü~}Ƴ–ËüH¶\Vc_µ[:°ÀQb7Éãé\’QH™¤vh€sá)›ÜËe.”Æ?Ãîº8w4Ãõ'û»K£aªZîp ^¨NK6xníDW\ ‡KçiÂåE¦@ªÝr·y Ò ÂÓÖ>­£À£}OÙ*ϣƹm¾¨Æ>]xTÆG·bw&…«òá^1w…áSzÑýs'3%¦w!ÍÑb ¯b­¦öŠÍh[Þº´¦þ˜ïfÅè«ûÄ8§ìÏ.«Žç‚¡â3üeÐþéx´•'œÿÔäÚ£M·Ä±›Íee&•Û.µdç×@¯h€$Yß>PÉUœ­’ë20‘ì¶{¤Ç{¾ÝÓÚrQEÅg}*Ã5ëõY›¬G®é’Vl6Ò¢(Ë÷Xà´?|.Ö8)03Ùã”xËàÄ6(¿øi(M€;"r}k}¾ÓÚJÍ¿õ" “÷õŸ+UÐ[ úbÙª>jùmØ ¡‹çåùÆ¥Z|šÛ@ë!4S¨eq8fœ 2¬öò_Û!þÂðæT‚jå-ܼT¦"1XœïNW‡1,뱬˜­«m‘]‘°Ølw8Ź÷Fû9Qac;ŠáRË€ƒ\¹ {¦Ö´`Ò`þbw½ÚK!ØW"ÿ­K.ÝÒ;ßR?’fq[µçÃZ®*^œŽl†å[l˜‡‚û:~­^Å_&Ï]¢]¤Õ  ¨Š¥²#[X“ø7ÇPô†O+Û8€”á¨Â 5K oaGÕrVáhŒsŠI›}—û"À§GÏØNÔ8g¬‚jØ\ »7ÇÑCóLn]a‚úá膗MßU`£ƒÀóÃ){ðÍI¿! •VÏ<óôfzf­Ãħ®m¤ØÖÆlqñ°“ï";nߎû=¯žÎØ×rµùÏýx%ÖrY¹¹´B¹ï„@U×—*tÝâ†<ÅùÉÞµfü ÃôºoÚîTo¹3=ä?þ ½@½K½·à´k åË©çú”ãË/ gÛGcë¹)mr˜ÆH–lbtï6¿|Ïʈ}Ñiáñç¹XXWhÜÄä"N{55W}wmÇtÂÅùår´½bË‹#÷‡šùµt\Gú7ÚòÙÃÖÇ »LÅ„…Ÿ“#Àùe4:1k/§…càá!QSS½‘–F<\Û[=ÓG=99ABC11¡²Àç™zŠF©åñn'ÓìúºyTÜB Jv³/ îûqv?¿¹Á,E9º/Ò¦ÔOŸ>!#ƒOMM­õHW 2Ç]àƒ˜ÇÁ':WkŸÂ3C%pé¤ Ûó¡,[29 çç8ÞïøE.Ïl_ Ɇ¾t2E$zt¸m½ýj¯£[®·ü!ÝDÌ´åR+~¸| íËß½£8ÿ¬‹3Is.Ç9*6Dfuùd„¾“½hS0$££'0†õÎÊ·è§ã¯m3" ÒK‚ÿÒkÛ^.3+ýÊñŠ“©1ÙÃa'2«“±ì|þ™¶Seá¯ÂCÞ³;å4Ó$¾)š~qŠÚÁ g%©u‘÷¥±+½»ë¨áhzþÉ&˜ Ä/8¤?2Û.Jûøøl‡ !‘ù`Ükhhèû©LŸÀep°6²Dœ©)%+ÛºU{~%ÌuVÖ-z‡Ía1ލ££Š‘ÏõP¸;ŽÝYLŸN@€ˆÍÆœ7…)C—Ka8ie6 ÈyšåZërYž®æIQCðõõå²™GrØ÷môGß »ó·×hiºÚµÀqK› p´·À)õW9ÛŽ³KDþ°76™ 7¦~ϳðÐf¨¼Š¢úSFGKwtéû½…gJE?T6IS”¿|Ýþ½ç£f@¦`}RÉ=œ™ß½D ED¢|4giÊõP”¸a KÜ{Cá¹Ï`ƒR:UÆœäåª%ô)· òA÷¶×NÑy+wù,««B½åý1#’ßm5Ÿ\ûx¨#ƒž ë—X.,ì}Ϲ³Å½¤×¾pð³ï0‘dƒ_/// ÜyMÏ>Kü"W;ø“Кüð‘ÝÇ00"û‡%|“ÛSñ§å~6?C!ãiyÆãõöáÇHÈÈßLùDØ<åá!t?;lÎRÌt£åTPTܹ8šiÁµ]êa_Yœ[S¢{üÑÛ»Ínm­¿¿¿Š¿¹Úõ¨/Wƒèý¯®¶;L¡÷êëëM€¸8ܘhå “èìèhø©œýwiÉ´X;Û{îćœVÓÂÉüî}™²¤>þþ{aÂc•'»8FÚ˜( Áài rÃZ“êæQ ˜v½$6d(Î‰ÂøƒòeÃ[j2ïKהˢ¾Ý =Qs1{ñiOg;¬± ¤tó]²Þ0[ ÆšÍwFO¿k'ø5ö‹9‹‘ùЎ׋#ÝAöE¸:gãåã+0ê&˜¶Áš)ŒSæyÅÿ»°ÐYÌ™’  Ål£?(;" Åê¡_èUçFt66ˆ5ŒGÈw+6:x>Iô›þ6Dú/d~Þ`~~~;L‰\2ëìh WPHHÄû>zË|ûçή^\²œ&ƱJÌSò.“¥ø‚O¹=õÃ÷'Þ¿XA¾7t‰Š¦E¸&Œ‡ý䯨 È¥&Xu,j|ÌgÏ)áä‘»é¾pŠêXRŠÍ³îa_+]F#y$8asÁ$nô ³¶Xë.°ÀMaâà´ð “½ÊPœ5ébéª,€ãÔö‰sãô¿aQs³}àÀ?jþe¬¶%’\’^ºõc_rÍz4k¿æyç^Õ’šû–*Ce5AµŸ#‘7ÆEÚ+G˜Íð¤†ñ§îæ‘ ß¸•Éë¾2V•ôX¨L™±"¤©¯.BðXÈ#‰r¤K÷™<ÿõë×ËDn{fã"''§í°}Îòòòå?(*9jõ<’±©#kÀ„›YŽ;QjµDÜJèYæ°Fos²¼*-JÜÒ’‘Ûa=Àb¢äU®Ê^˜âSÓbXk8tŠå,6k pzy¬šÃ|´Ž'Hñþ}Èyª•½'Û¢ñ‰)²3¼çQ^ŒºÖÈIžñÄב(­m`^ÆCZ‘å›Uàî l]_Û{pRÌ:à㚨’ß{l{¼Ò"øÈÙ©•–ÝWxg5™4] ©ƒçU›R\+ R%ÉV}RfmtV<+’36´üú2k´Ä¶›iùë™Í·];Ý_ñ–O­þjû­vÀ´8†ËšxØvž ±“çË™èÀÓ‡RËvôh-]¶„àC8  ÐµÔ°JÈ)Wj©• à™ä¥‡O!æfO•ö©o­s_+Ùóò…*þG±p’0=¥ÀÀÀc u˜-'¹ÀX$hïfÅØ½EÎã«ÕfdŒ÷ Í뵩qN`‘ cZ¶æÞD­yÁIÒû;y'LߎSgº¡ E1"»g8C)'ý+Ü%ïÌûÃâr£Ò'¯ÑË÷Ô[x>xÙñ̾dÀѯó‚á´"!"&vn„+Ø¥íáq™í™8^lÖÊÒÿ®XA½é3µì"q³ysK 933Æ}t’yb߬¾>9»å>øhF­9â…D>Ÿwî““‘}pŒÂRÌRÚ 0xDd䇆ÁéȧËóPÌxœ…:>úüW „´”ºsG =š%lFÝõ1k*¿¾>,[l–ËsƒIY×:d–'2Þ æ,1ìOÊßѰ0= è  ޹¥ö¡ŽiWG·»ZG>…½÷Hß5y³±Ü¥JCïù îSŸ4Àåf¿†)_¨ºÍ€¥v?èeEBLä3Õk.*¶ …@ål•·@´yj?1k ';Úœ²ž<9¿sJlðNË_*ôC' ûñƒfZ^ò:1·]pùMW½•¼nÇÉî‡oÊù˜8ž>E¼8=ÒÖ&™üÞ‘fIeJËÁâļbÉl—e«Åmòuu+X#Ü_JÎh2.ªtÍš£Cýö\C 뾂`:œ’õSRy¾E“ñ{؆Wܳç™è+4QA.TÔ&`KfçæxÁ6ê•`ac_n988ÃÂä²UžÉ^Æ+d5éÊò¶`Ù'â\³:¥ï@Sãñ¹¶wŒÌƒ•-”}uóv|”·ûüdôü/ç¤{þQ?Aõ=Hm Ή€ ¾öò ÅŒ—ÿ¡¼&P/ï‰Ý—òêNŽf9VRRRU$jŒV^‡r¹À댥üSi6byÃŽ›ÒØoýf:£)ÕÓªÜV•Q‰¸0XXXªÒb|FGÕ¤ã9ÐÈÈȪҪ¾¬õg«æ³–ŸmïÀ(/ñÙï?xð°VÄ›C2°}SöôÑ•šâ'ßq÷¹=nYù©g.·>ë`àíMcÃï–ÏãÆ]#çø»ºg=Ä\™CeF+˜7ôd}°WÁpVÛ”«èô·µ¢!<–Q{Fœ´ñ?êÅ!¨¾çèèx 9…iJ5'–rèèèPÝååå…`?î¾¼8Û9YJŽ6¡N+,,ÄÄŠýº6’û,ËÿJAk‹†Š’–öÈ8|™$Þvd8Ýé*g.‡u¼-¼ô ýÅfª“e¯p+µwç%ºbX¼Qc0Ÿüu›É)ÃV2bœ'”L-Á@iÄ¡ŠÐú!XÀsLÍî,¸_ ðõʶD¤¤kÙMžÔÀ÷¿±xJ«-àÁÎÑQkŸ«\¡Ñë ø¦•Òo2}\f u†©&"xìW!ܹ¯‘Ó:6>N@ùN\B¢þ‡|Úöù‰í´¼´‚Š Î=Th";wD;=f/§¢’€à"~Æk pk>!léì²M‰ržj.Z£³ãÏ@{÷¹3¨'Zžëàgi–³Çâ‡F² L>æKih!³.(âMÌòýË.“5î„@׈ÉÈÈD’]1ù}S¢cqÚÖ,·õw|ôùkRR>›ÑßE~cá…ùù¿™ÉtÂä‰2þ˜¢&³,‚”ÐV.)¼(P{tˆÔ*ÀamÁ‚(øl‹"`øY;æ-<ª 3X6Ø3M4ŒÔZ©Z!éÒ©÷(Ȩں»Ñ̆sR©(@¸„}±š‚§Á fîI: S¯`¥úîðÚ—ã&‘QoZn‰ ¬}ÿôwå€.(!Ã]^ý2‡ ‚ï²Éa˜‹£ÞÎP³=×oA5ŸªøâˆúÙñêÒ½+ŽWPiz$±¤¬r_ª¿%É&Ë´\ ° × Há$FfíÃ~äáûÉaé‚ÚWøÃ&¿Öì»5  Ê¿du4ì¨ p-za1WgÓû`Ày£8lNÙ» 8o±úE|P§@n7§¼òÆË «ÉŠÖkÿD)bJ·Ú6òçô`DÇz¹lš¥«2ÀÙ°æ:ƒ)šÏÔ$¾{­à"FTöЩ•™°öê’Çãt…zš¥Í,Jr›} Ïš1¿…Ä’z¿•ÆF<ÙB?±Ë›&ì á’U3'‚]0¤.m.¢RÜ~×]!5´¤Y*SCDãé< tl”#œ`›ççvµB#4€¶þŸ©W6qÀækÓ32È()“3åh~Ù,H&pÛw<í=X;€Ìâ«:`ègNËIH ·z`MM¯™n–¸ì zL¿½ÓJ óFç×U`¢¡AîBÚ³±­­­`ud\\OЛ7•Ï€H@ %%%æ5n UæåtIÓ7/¤s¯§àS á¯è‰Fí·­9F€¹â߇½©þMNu†¬„pæOb`®Þ3¯2ÏÙÌ9·2¿‘Á‘‰™S*È„‘ÕÕäÛ$µ¬žË8*Šˆk`’Jg<‚#"0®íÆg¥¨Å9s L‚ ÑP‚Y@x¢Òʬ.ÕÐ˪"C?•ûK­f®Îy}ë™bSË0 }:O®ªâ…œ9: ¯—¦ådåÙ®@tÿ`˜}ÀÙÃþìðŠÀKáxž¿ZÊý(˜‹õÊqd;ìí#KîžâPm±\E}‹c“»M= s÷24âjjÂB³Ò"€ÿ]“Î{xüÚ‘,‡[£Õ|øìK±E3ÏÞªw™r‚*vüñ§jzw‰ˆˆ~Ù.Éü[QTlì›ù^{GÇÖb³‘¾ )\^§íº€,I*@éþèþŸ?c—Z]µôFŠ? QÄzê €LT·³{äªP†ø¹~O …ŒŒ¢â7í‹‚ûË~Méã»õÃOâO¨æ“€Çöý§­:y‚$.++,¤™ö÷íòÊᆰ‹e3~rz0[5\ã¡BI;%k%+Uq‰À¸¡Ícˆ^[k™z¥ Ùem¼"My.s·( Çù=Ð.CËõ<ÈõÉúŸU§ß …j7òµGP.kWòïlÜ%Œí®$}ã’í}—iÖt¡pu† Ð]>j -ÊÞ¾}‹¡0Vqû.À·ñ]Íð Å›”€[…NÕD.ݬ¬¬ˆ¸í.ÎŽZÁ¡19À<öC ¡$H³‘Ö©ÉI] ‹Ðós·‡+¨Ù¿~aT:lÌÃBsrr000à Ž¯èáàÒÂÇÇ—ËR"¸Hd~‹äáqËRP}O=µÛÈ¢ñ•ÓйÝÀ“‡‚tqjïãçgCZ™­õ˰§+‰o‚fÍjªâ1`Ñ7•éVç¶_­zÌÈÈø«¤ÄãÀ þÝ»wâÒÒM…ƽ˜˜üÑImdº†·½ŸoÃd,!Ǽ÷( WàŠè™z‘—¯o³§Ç´€ÄS,µ7oê;:IHHÀšî×\]ºžNX–YS$ñ»›®{‚‚„Y±éžz4†7¶çÃòUM˃Äg‰ýßllÝþN#(›O¾¥/}6 [KeT/÷ôоKëôõ•¬7 öö”)¤wNšIºu!={Y¥Z*køÝ2Wnê{9®ª=Û/ê”Ú8é/3ôõ?w0~e”—pÑÊÌULË“"æ"0Q2N¢"¥Ö—%4x&TðõMãØü0äû\ê\ =( ÈR†Ì"S»^Úx°þAa‹Pˆ1Js$t”¶ä…Úµ-ÄgIèÃ}ëTkÃFóÔpkEÁChW¶çF]x ЛÝ}&&¨ˆsðóóC2m¼è7 “âïöœÎjjxÀAzoL”^mÌU “ÈΦŸƒÁ^|§¯†úujÍºŸAa2’GLD<ã>Û DUOO¨ ѳÃT%:ë¥b·«‹#§í¿ð#…OA*wÖ022’²ö=3:r¼§Uøô)ÖƒŸJYs§èÔzlù¨tŒü1dÚÔÒ ÿÑJG&`~Z«ÉÌÍé¾ £î­o¬®Ê€“@?ßã;*ËŸ7 8@“(~—~¥«ûd±'1øï_ÝCæ-Tt_$sÔ‹‘éUóÜô…¢0žÂhešwƪM( _†«,ÿ%F²âÙRÈ¥Œn›}«q—¤Dyûƒ+ʺ« œZ8¹X¢~J‘ˆíú$‚²{)A¡®#&óãµ%:Zé{DÊÜ)µZ:x²¥4ó謯 Â4W_Õ¿'lÓ­´ßJŸlÎ.Eû­òN¹$+D4­ÿ—œ)ÏÈÇŒÁÝÜäü~³íçlX³Y˜â•ág+訬ïêZS¹©Øãàã;2´;DôÔ$ŸÍšÏú¬>‚‡ûbAWÿê-zbŠØwVì,ßwm Fddœ˜ø¨Ëˆ—K}¢éSbIÜèÈR¦ÌX{"²ö¥ ÁÃÕžï·O‰È@z‡‰‰‰×yêt¥ ùK›®SÂñ»á͸eÔ:]m(Q¢ñ¾‡†ƒÓòBkÅÇÇç +ë' wOOOé$^ü;÷Pi†¸;›LW½èIâ F@ÿüx§Ìv/º *++K­Ê‰è+ã>{t".¯¢ç5KoÔÕ¡?›Ããwã›­ÓÚ±`s|z ’WÈÿ¡H¹ÅÞäpÀwlVu6À-%ð¹~}÷:Pøã¬Uÿòü$B2Ô¶ìɸJÇ-¨AWà°¡n”?`çÔ0 @4ª$–t) ÕÎY¸ºÎll‚0!ö³Ÿ÷t4°Ò¬ºù‘3S–F“±p¶ÿe¿ìÜ«2˜”BcRöÆ„KŸûV-£.ù¬?EÏ[aˆ.ìaì~ÏOìêØ-í jN3Õýí•°Ò…ÓE­S+rìN¹‘ò‡«Ü#¯<÷»£K­¹/QóøjVËý>zºÁ7¡'|1KuÁÇö%!tŠ`–bëå }žbºQàÝ©†X[è¸$¶CúžÄ ƒÆ—PÞ×ü ÏË Ÿ¤M»£’5 s?¸<†Íô ’@oo[°Y-¸£ãq’Ñ~a9dZ6:š"“ÈM¤ø%I;¿õO†Tdttk†ÇÖtµõî|»'îUÖ›B£2»,à(,ûìƒa™Å%Y'}U€·–ŠfÄ7¯þ5Ò¿}ûÆìz¸®Ç‰;“n kázÿ.O³\¯ˆÞY¸£B¬@D³‘¼f­æE@ˆ°âCVWâ3¾¿|SsóÛ£Í) õÄ™PÞóÏòîU¼ž"Ð7œ0}ÇX“ç—†ŽãÆŒ>Dž ;®êyb¾Q†™||Èa3^*Õ#»¨žñ‡5—žÚ'Bom äë}c㊠¨9Cû ääæš©8˜ž=C+4ên£}Á)ÕY®Ãj¿Ô›¼Ä1w°–_ gÈ¥ ÞMËaú" ¥åùéZ¾ÁÌ;ÇÍÉzRÏó·¿æææ~÷’x窱Ƣã2µô§‰É—ÿžó`7F….\Jö´Ü.v®°]z¸9]Ý™“$L^‚Ël>ÊlįۓðÝôQ¡K4“ލ%c-Û:m ÊÜÈn§£¹û›Ï˜sW”¼CÝp]}ù¾?C_Á@f¾½Î@Ê'ttÈSöŸ©eËž»¡Œ½‡ë+µÒaP/ x×_sŠÌ粯«Ž ×ÐØˆöG ñC§“R–RP 65«Ùé ýþo"ÇsÁL1ƒl%™²lÙCß_ºíusv­^|ŽŽÆ¥’Ž©KÿúIº¶Ä'JEòö|­Š,™|Šl¨öƒ–œÅµq×b¡àŽ-¶lÜHœúœEs"Ö/넬¬¨Š„²‚™ê¢ù†´ÅÂѦ‰Ì¿½8“N‘ºØdõ±S¿ž$Пbú«—¾‡ìzl'¦3#òûpâé"%™Æ—î*F2?È tDP½ŽÓâ‡|Ú=`1Æ>Kf¨ýÒ…÷¸<1VÊQ‹tL¨-wÚ!ç°œ ä´ÜSÐÃeÒ¶a`#à´„N¾Ù-ÍG5å×^ÕÕ½üÎþùÓ§÷=ò?GDxŽÿD¾?°Yïù»?ˆß_ë«´´Üƒ8r¬6Ù¨•–$Xk¸>T»³ˆŠÌOŠML…ýÚ è0ó:6â±Ìm ZÂÂüøô3Š8,Æ}ˆy¶ÃäõZȽe2õÕm·g' Ä<7Âl­­ßª(¦õãd*ï8PxWW_jÉ?í,ØXFFf´Ê™m¯Â¯Èj†ß­%\àä¾Âš("`šçÏ'çç €#¬~H–‘1œeM»ÒQ)Ðÿ2Ç@帩ýÈ6håYúp‰gÅ2¡„í‹®Ñåòkƒeñ6áÎZªÙAˢ{p}™÷ÕUß= ŒE[,9Ƽc†þ÷“7n¤¸|ñi}Ý’VM`Æ –V¶#‡KâÈÂOÛˆÌv}õÏBtÑh¢¯0R†âfá Býõ¨ÎÏÏ™±9±Ö¢¢ëúáOßþǤNb'¡ £:/xH⯯¯;‰9SRPÜ[|!#–=¤7Sã.í8äl¹óhÚ=‘§°ÚçÞIåŸìN œœ&}OŒŒW«%‚qžÅx森Ìiëíåt! H¿ZF˜d½‚&Û¦¦¦¿ÀÄŠæ ‰¿{·OÀÛf¨’ŸÛê/J¨£F„ƒu— œÃ@eËÒ”kaÃÔèU–29+}óÔe1¿ïg_콊ºäгȃÚ& œTþ´–rÚw«Ãˆ| Rˆ˜.'û^šäÆ èpˆpΦ°ZcŠ&µCoù\ >¾DÇ=©×¯ˆ?Ä`¢££¯O–c?Sß&|Ý àòöÞRÆ·AºÔáØ£ê@ò¬aO”ZùU¿Ec´ÕŒjèX¯È€½þ!yåî‡&ˆT€±:::)?Ã&¦yoôîÁÁÂ4ê=¯•ÌÆöÀÞæ¦?rÙC÷^¸ÒTUUõåiñP}SÈìr %ås‰’ ”¤ ÝzñÜÅ3Ûqÿ¡ÌÙš÷˜ûT‘ƒ‡6و׷§ËÊ…W¹7vÄ­7ƒ„ r§råÁñžù@Çð—2£¿ Ý1Ã…êu%Èqêõ炘»¸ǹ½1¥¥œ*yš£cc,åϋԟ!##3›3-kmcÓÐà‹²3 +@öô鼩jö†Ïï3KŸfM4äÀýÉ~iÃÙ³†kaü×}gÿ<­Ê¹ãØòL­‰€GOؘËù1ËÀT0ô'èb¤;øf²Ðk ÀÃÆ ¬”Ù¯©dµ,òÓz\œ c¬¾÷:ŽÝÌÏË .á…­Ÿc”^`X˜ÑoWL¸ñR+ô@á†tùaòýåþ;(8 Ì.¹™²ÉÕ÷È2¸` ÕóTò~4;Ÿ(Sãnœ˜n(“!ãk®Ì¡É0úŠ)ßi]ÛÖOà`£±hÿ௥"~MØz8çKâþ7,]kXza 50mí$³UrZŸ4-Ó©æ…>y•«½È[ÙV={ˆœ‚¢‘Á±üÓ.™j¢?Ð'F?GUüdòýro8?R¸k÷‘$©$©K´IÔ°Yλ¿MM ¾n¬áüãÿ×ÞW…E¹Åïb‚J·¤€RH— %Ò%3Ò-àЈt‹„ %  ´ÄÒ1 ¤H Hw§4œõ¹ÿîsÎå¹;ûb?n¾gÖ·~õ¾kýBiiè®ÖÒ‡ Xo±þà5qLÀ¯ž9žÒ#ÿ8•ùŠ&yyw¸™Y °h`Û‚¾ç¢¡Þ¼ukzzÚ ÞÝ(9(¶¦ZæA×*´Â.R#0Y}–[s7jhÉ€|¥|ÎÙÃ^®J2ÇJš;_âçàÀN-’é†8è^J‰NÀÛ°ü.Ný éXf¥ª”ǺêNNNß—G Ã#"Z¶353Ãxó„ôéã#^SïQG×××7`r—ù)êúÕ/5¶k¹:%t@ u»+Õ¬ãå»”€{ÐýÍûðÓRïÞZÅNо3Ö)ûPpdW•ÒŽ/à¦ü+‡®/TåIY¿—VS–³¨"æ•…bÃ à‘€òe Ú£¾vvúp~ïVùÃùTTT€"^§÷9dˆì`zýúrЍû[]1¨Êݺõ`<ÅwgkëUr25 úIà%Ôs9úænÖEËEÓÝfLv]ú~I+/±'¤î¾VÞ=´µy¿ÉÀ² Î~ Ùpu‡Ÿ¥fÐ1Y¬^Žl¶:ƒ¸8]$µÀmàµÅ'æš–Õ¡ò&#ßyù¸wŒ¡†ÕýûBxâ“Bä"b­½ób€æ& ݽhOè·Ô{fóŒ5k*Â*ÐJq…Ž›ã}x‡¡ujm}}zc#ê&Ù}Uç¨gwijÏÎ`Â?éWrgÆ$ ðN‡u_ó–§§§ù~XU—õüÂB‚[åÝU§ŠõQ´|VãØØÃ^KswºZ£b9##£P:jÄÔC‹°Éâ•ÁüÝ(Õçz…äBBBµˆ8êh:±qª„7™C¹K<»²Øûs3lž[§R]©[PÀ°¯%2ù²=1ab?ßÓÂÑw¹ög oÔótJ•ÓD¡ëØmZàSÅp°ø¬L´ó©øm¿–)((„Ò½KN¶™ºë°83Ó\n¤¦™§Š `ì·|øõë×)°nÛ3×Êv¹n°qÅGý꽪j¿Ç'PzÙŽ¡ßÃ^Û+x÷âkz GŠ çÇ]î0W—}ÿ?+ ¼òæÛs(c„–‘°'÷4z¬ªšÞB‹¼?ºfm)€fNcúúˆcÜ LºzzÌ'H¹zˆÏèû®Ã _ªù³–)Ž]X2ú2^òîhG¾—’x¿Çe¬ºG1[Y}%5l1„ê<÷½õ0yȉMÔyîFÁKÁOýuËQÁ´¾ yrwž€•²È€âö계\Íû9"¥7ØðýÚPÊìh…™œñ·„RŠ—êˆÿ¼@s#Ä‘›FPæ’´¨  àN|)uÖ‹£3st“¨hŠUÝ[þ—uµµ¸)së<<<‰Cìa{{.DaöÏï˜%Éßåù™Ú“#û ‹hÉF¦¨wYÑŒ¹ëù«K Žÿîåòì[¿×ǰ°'ÔªJÊË ¸,´7–YtY“ÉYZ²ö{hkkßó0ÌÓJÚ¸lTl¡]Ì ‡ÃÝäXyr5 ïÂzÄ• Á›ÇVÞùêÝEÞ|ŠB¨"þ1Ôs‹‹Á®«C wî¼B’rÙÚÙ%E=éþúõ•zîsðeW%M„ojr½{Ì¿T_KªP›²Ô |zuuu×J]¸,ª ÐC·vVžj|èžÖWgäð‘øäÈœ½Ò¯p<ñBFª[3+ä-±~©ß(ºýH73ã?‹ÀÂzÏo†¶žÉ/®‡‹ßÐ××wY%‹¦—0Wçæã#CnŒKî.t' °‡ÅÅ‘æ,»{z¾sÃF••9‡&ÇÞsÚ_ѲNêâšLL¤,;¾MŽN–^ûûg¨çF©Ü%nš˜ q–cÛ¹ËÀ ­>ݨJ©í\ŧe’ îæÄ!2IÍžB’;¸—LY>îJF†¶;;µ!ælvL9Ë̵cIÞ¬ùÆMxËý¶p,{(­Y:)´ ð +[;på÷ ›[?„Y¦^v>Ï-Ím’Ça¸ðû+iÇ ÏÍé\ J^s\°1îí¦9z|/)¦âîAéÇf¤¬¤7Í]'ÍW‡ ©#@ÀbßN’! p÷ß^\qµ® T [yZù<ùû[Ó?mJÐU’’ïc½{Óí§,ÿãÀ}è4Û5uõ—)rˆ‡MZÓ£lé…N}ÇÊÜ®r.€ Âw\Ãþ1]æ¶ÎÎ[Áø´R½ibîr¦9Kܳw‹—ÖÓ£¯­`bl¼Î“ºÜ.óàçç·¼0wô ¿¤Ý…ú•„TTå ÉЇaqsä¾vŽˆ?âX]Cà &3‡³h־хÀ‚’o–7‘êçŸ}”¯4„U5PüØÄ«wŠ9Fþ³Í0×Þr0’”y||\5[Ì|ßE˜ÿ’„‰ƒ^$’„Ÿfe-Ñzæ|vvvD„„ªy|ïZÁ?<”!¡¥HÀVf^ó®GÖÖ÷7)1Ìäáj•i™Ñ€,0+&b¡õº³è²ÜR8hLÐ&Ú›nk7ZŸK ´‹k‰xiÅØŸ—%B0Œ±,ÓQ|›±ÿ=0®OH¡“‰ª¬¤‡ë•“ßkjÊ\ïP½iн¶".K„†›“PfJâÔc‰ƒCÉÐ39IGÍÿP53SÈëðÁ°a é¼«%˜TaW8ÿèo ÐuG¬^À›5yΩÕF{Š/wpI])­i¹˜à¾¥%(_Æ[¤.›Øw>æÎŒšrŸp^p5þ@Åž/¡ÔW (aé#1µ² óüýÁf¸¨Ei8\dÈó¹†Ä¸xxöd³`º²÷ÅPV!ÙÍ~Z'¨ ÀçÜáí½=éùÛ^ºŽŽŽæ¯1LÃe o~ïÌ?…nuºV©Ôª˜:8ô©ª¾pvNd 4àxœ@¹Úš àåèÄ!îçðÛ4¤ø]+Ùäï'¸|èz­°¾jš3 2¸ÿjHQ© 1ý(kò{ß}œ¬;Yõßu3ÆÈC7z]ibÉHR”õh9ÛnŸþboz{À ’Ä¡ÓÎZ÷ÝÄt­Å¹¹:@6=hä(ÐíÇ S&¼ )âl[$B ºZð<Ø€ÛÈkåk›Uøa7D½v¶Â!‘š(z’ÝFè÷kS?ÒÈ?éO˜NÒ[`îžARŽÛ eœ´XË´ ½›J$L ûVžcô}ÖdÀ´ ¾’<©CqÜÃú¢FôJ¨ûEgð¾ÿ|f˶ˆÛ‰Â®÷Ùd•5µ± ç·}P\c7Æ¥m[uŽ8;ÞïBªeÖq]7ßûä·¢Üèeß aC´5”Z°°ûg¯£æû¡ûUóY¿°Â¡17g&çÔ 89Ø„N £nbk¢Ìõx³ïIrI–¿äÏwµ¨ÇiA¾«ooúôØ\ #~_÷÷®ˆ‡÷!AÞ1Ù¯3vbØ&Žõ¸öþrA-I@!AE†ªþáJƒ\Ö¯öÌÏ·”í€'ø€S[?ø'üÂ3É9t^9Ô]OHH*…|yzªÑ~€‹5^¸Q|åçê›=©"ÐíLL\ë¹ò.@â·m ™•ÞB¸®_üéª$ÅaÈ ƒOƒm5„ýË\W4¡ÔÙü¦Ñ©&7©ª¿¹jùÚœ9|Ã…úÁ›õ'QV€1åjB5)®+¹3Í:Æ{á÷‚­'ªàÉwxF]¦ùÍ>Ïq½èœ`ÁùRI¾û˜bZòø±í—2ÏôYÚ|,øí,øK43™³Â37Ñ~HÒó6l\÷ ý´‹yõR«Rܺjù©?ÎöJ±œÖô”­épT£i†cRŸ)á0`ý E0+è\xDsDš Â$..Þµš7hÍ¡[zµÌ¼›gT^0*Ž äb·ã½\5ëÚHÀº…‘¶Ÿ÷ihiû*ìô¡[‚×w¦‚é}—úP1oÞ˜lí;ŒWùE‹ã„“sòŽŠè’pê•›èid«Cº‡ke½ú,‡Î?E7m|u :¾JŽ?Õ[&ŒG$½ä%¨^_žûƒŠŒÃ0q8²;R ¹çÚ¤{—Oˆ:._%µŽ¡pÇr.º(Û^ü0hcìíµÙï-Hãç^ôV]~ÎQõ³ˆñÁPçðÕ K%Â’mm‰[ÊW|빎¸G~[Ѝã‰Êj³,¦‘ûÝ~fQï áÃCÈÙ¶|Xv½ûˆ’Á•â8`n%@­«ª«·£àªŒ18ƒñëÄjÂÍŠO¶Ûé¡Í–•¥rùåæÐCW…¶¶’Ã\y9JÉÍ---l*lþø°J|gä³é•ÚÚÚÏö¿„ýÿèï‡y:>´µèMèw6ö=ãÿõ+iøWµ3ËéÙs˜Îö<ÄzÒ%¢ê<2”tœjlHcLÚÄäNeº’”Ûçø±u"\hÞ¸¤D7Ó› FQ‹î½I·<îÎ1ÃwÏ )_¾¬ ¢qíiTÀ6d$ ÖécÒÜ}Î\›ôÁ+]+d–·œÄŽdKè?’¸à|©¹ž¿Q [ŸÓˆóíPh\º6#Átp¾ÄŒú[Óž‘úØÜç“Æç—’: ÅdQÃ&˜u-ÁwŽwnrYõoº ]©âÓ¾¤x lbºÏ1›—·wõ`ƒãôåËW®Àœf¤Eݶ>hÚdgeA·HŸŒÄ¡¬¡f)qìGä r£ÎÎêˆ'Þ©wp(a𫀸ÂlGŒ×º“(d—~ ­Ýï öw ÃÃC¨'¡¬b§1ø§¯@—— M/î"é=ü ²AÍ—Ê) %aiþõ‹Öð‹KS “Ü„°ºÛ à¸r˜™ØžQ}¹Þhdßž]"[º#ªÇË?»òÙ0MÔÀvÅ–é%¥çKÙÔÛáþjÁ1ÎwžMÜà gFIßõ‹²êFõ5ÉåÝÃ.ó5i]† ’!¯…ûÎI¢Ìl–¨°°¯©\Ä%&‡È˜6€OôìL ˆv¸Ì¥&_¾*ÚÑ¢t¸0îvê¸/'úã|àÖ¥Ë ãzQw/~²)â°MfÕ^ù'I…å¸ï.@Ê•_Z²œpÂmœ% µIÜpñ040 IàÐ}}¿±·—J×ñgzzz{ˆ—qwwwNii驵ŸePZ$ÀA€Ä-_@m'e…9zÓí „b¡ŠAØxëé¿í •iV:Ð À¯³è¡®Œ²0È€«J0Zïü“HNÁ|gîÛH§€æ}²MVÕüAA×R€[ö‹£C13Ï"ëÁ.¹ÈPó>£Â& ƒ `´$”2ÝóØš3_²¬÷ŸqÖ¢jÕ?qJgTrïSÚ¹X«Írþ,gª¿A鯧ÌWm~Äý'ùkŽu ôŒ¶ Ï†0ö ö³7ìl¤ßÉb;CŽZ³¬ØzËÀ5Å…Œ49}5©ô0gA¹ÃB"»‰'yOTpÚ¤,þ"² (UаLà¦"“fÒˆ5ëJ¿+œõöþ°1uÝÐH B­.ƒfÒ¨´‡Ú‘«çZM" †WcRS#×Öl™»e¦z·} vðá¤Á»‹…[:{b‘¸±ño’C€{œÒ½AeÅñõÞ‹*½Û>ãŸ`óV;Q…“h ÍÏ.°Ñã…%Ý—mi´Á'VfˆÑ ­.ÿ^2BÂW!~PJækm==Ú^~B!ö´¨2ðB>|F?ç^ùNcëþYGK±êÏijn~ –5¼º=æÐ0]Ö+îoT¼HLˆz–M©iqÊX7“¼in”`ÐO‰}®‚NUrFÎ¥„‰ø‡*à$é|0î¨6Äo‘h•èíMW$Ú¥¡fá¢þµ÷®¡!r¹(ÉÊ©€Ëóðו*ò®4¡1†HäKùêÕ+,ó}ùe!!!Óþ%GGÇ)ZÌÝ\ÍûW±±¡º^(eÝEiœjúqô0èrE-r¨èÇ⤮(Ææ1ëÝ…}£'ÜK%»Ûö`ÏÊÕI´Z®&çKÙ1hÿÒõGÁÚ?X¾ghÝú7Ò'•¡Ñ˜Nýïe*j vß?HRŠº=ؤ47ƒŠ¨„œ™ûß8¹ý»Ä7ú¿d¹ˆÍ‘(¦‹§BrKOÐà@‡ó´„(bɇ+m3bï< ²€òú"Ý<Ž µóŸk:ní¬6߆‰’IlâÐ2„¦^þjý}ì«Ée¢Jtü““a®ÓÔúlÜÚâ`ÿ{ã~R͸üúõëï ¾¾Pa++Ž˜ÇÞm BvºgyŽwf£%deÜk„òØûPr»Q¦¥<<<[ókYhÿ4‰K®¶œe”*Ý›PS= ¸5âåË£j3ß°ªv×SA„ç2ÿäžuê(ëª~gÁƒÌsGÍò<“¸ýî$Ùbɯe³#Û–;îµ³„R„ Œº¬fžæ j{¨´»Y’bЇwÚŽ¼A’Æþ–×upp°¿]¼Âî²1ž¤ï­îE !x©w\†nr0]wwwJaÖßk?[€öÈÎÛ-r¯àEv.¼/+»±XÂ'‰Ó»·ÉZì’7JDnÎÏL†µ‘5[ù™ Ž?¸­[xþ&,[6ß´*MnØŽ’Äa{üWØò·á€³ß{š`²2[‹ŠS˜ïˆ/q^TÈÕ€Ê=øL›¯zm5að†`àPQ´½½½ó›''§¦"ÚĴ1+{ÀÇa>§n)"È($©TDžû¼ùaÑ7Ò2‘þhô­PŠ’³m® ˜QîòÜ-• X˜Ög.8ÓÎö1™á<…§{E¦çí×)õ‹çiýÕƒþúÙ7<–M¯¯™û½/.探SPjó% ÇÁm­WNîD]Æ‚2€¡tªÜ¸—·I¨©©QÌ‘/ÐbžßcaAœBÓ ¤†yÆM¼ïÏ´Í݊蟣¢îÌýtš€aa½|!²··Ü#"*ªàÁ»ÝVãþø~B¦q·Ê÷â°oET5|­8 Óµˆÿ[®ÝI¼Ø -Т4ó´òƒü± 20J~•½+MŒš„YqŠvÞáËËõ0!ç…ÆÑ/¶‘?c¿=¯e¢ŒG£ù*&£t¤tfv*g™€áÀ¾“>ȘHßÊ3mM&ç~8«Ñ“k_µ3—RÄGËU¼òlò|zxI:A>|’éö„¡Ò²ùå•êÌœ,‰)ƒ™¯½CÖZæ_gc6q›÷³Ë²º °ðN”:¼¾¾JÁ8púPëîæfQ»ó“Mw9w ½øPЦ­­-Xˆýk“ ô~~~ÀƒÆÇÆ’hEf<Þ‰‚¿…^ŸVx ¨ ”~'}V¬ÚTzy“•ùÉU"k‘Á×]Ò=eÏFPÁ*0…»ü,ø‹Ä=Ôè EŽ–7_ÒQ΃ÊóçŠm_z\äáê(pB5Üÿ‡GáÑ.йØìÕûlgw·àÌïÕÎjU[৉:OØŸ[ø ëã½åDa×¼ŸÑOã¿=‡*±¿¸,¿®°Û‰B¸éÔ¼ä\èN¶*& ZX\ †Ú"lM5óoV|z@°Õ£¡Ö™Ë V.ÃAŒ²ÙçJuã±}žã̤´« ÔW ®Ö?ç¶ö¤u‘™¤s·íÙ&滇q£¶½Í€^NGq³çáöýRŠ–TˆÚ¿:üÂdªÑŠó›”Ù3ËË¡SS&^ÛŠZÔ&âÐ?å0sßb-‡ no Êðx»(&ñ"#ˆIÂ1–}™× ÓÇù€Q´ÍnTkÀÿÔ+ÛŽUD¶}»ô§ MÄËõÑmèLØ:ì¿j8^¢IÝ:´ä V0AëéMÞJ9/qY[}WD‚·ðPªº(»²ÃÖ©Û Ìw xˆiü Ÿxv®hÍE,ô÷t4ޏկ&lS33Š £J÷]s(ïC+_{žóIš‘:‚’õã‘)ð$ºncûððæþ~€-½Ñ±ÜLLþÎŒP –“C|}Ã@Þ9yïʳgÏ Œê¨Ú£éƒƒƒ±7™ÈžnÛù—P®K“tl~*Âû\Ð /?°µ)mè§!çTßt†¤áRëÔ ÷2{÷E‡¾êº¨,·=¶pü:Ói½Ô…ÞÙ¢'B†éüfñµ¸UG `Œ&Purš¦ÍÖ<>ØŒÞ]ìEnMåNø„bÇéækŠï~³môÙí€À@Ú…­b#Z‘—k¯>~\Ì¿HˆmÑC½ès”’÷–ú¼9;¾øœ[ºE)ô"ë?1ŽéÆ·Pr:ãÖ—Ä‹ôê{]U§òê[[ƒfÕ~æðl nô0ýãƒþ4¿È7×îòarCköm£Êè-ÃY¶%Í}ÄáDý+^¸ZŽ÷WÕÊrÜӋ瀊Aš=Vaé@6›òÙ Qvc¦çg–?Kïm få?ã†lúÊu\à¡ ¡û˜:σ™³ƒÉ‹±öŽ1ü¶aוÏŠ šº’.Šë<ÃcÊD Ï»Öú<›WwUgwá¶Üm å4¯ð¯ÙR´ã±½Õ– ‰»uÿ&c'ûêð«êŒ™åZª\™Y&×d\IàãXÌYøw9+škÏ´Œ>¬[mÙèÙËdSðÇ%˜úµ=}PSúpõ,-ÉáåK¾‹MïŨ9ºý6Q Ô€š,2Ê’&¸jLaz7††Âx¥Vd£ætb‰RäãèlħQ¸GZ9+]ýÂ&âh­!bè˜wq—|²—Bd3Lß"Õ0]K,×JƒRÂW3ZW¿ RdÁ›‹®ú7ýì,Å/J4¦lWªHB <×7'%0rõåHq˽eöJÍɾöõe,,ú¯adìP‘¿4âw­êº“x9Ôÿ{6J~´3ÿ§ƒ4æBÐeIWÓ³œàùPn‰×ÑÎadzzbZZÚAºÅò÷¾>^'“ÑÐ0“ú£nYccãQ÷ÒQDª®N,Ëjò¨™‰FïV¦G³S^Œ€€ü§o4 ŽÎc)Í2Óèwen•‰š×\±`¶g ǃU»="P’ü ÝË}>×íP+úè@îHtïí‹/ó¹””¢5>ÃõéÐ3} Îð¢ƒj!e¼=ãe[OÕÀËkéûª¯«M<ËuW8}<üVˆSCF¤’ø"1“mèì"·«y#§CÔZI³.:||Ƈ¶“ª¬;Èé® öBÓLÓÞ 8.ÉI„A}²¡>rñ Ù£LÞ–vvvNÆHÌG‰³e·Óíö?̓]Èö­-G´œH6ØK@ª¼~—¥GžlÔp稤­ÇÈ*‚߃Ú9Bûõ„æiiE7½PÈVÿÓCÀô³8càLÂ4R«C"…KâQó·8@ýªR¶Ú@<ßH$ ‡f!ÕŸÎs·CxxÍ»>ý–wÔNz`.uº©Ä”ê>™DžŠúÔ¯&- ›£Ûå¹ìíÓ¶)œú÷T´<ãúgÂS‘˜@ÎìÉü b– MϤU‘MTCR%OBWÕQ„Ž˜ÛzàpëÌ·ðÙŸèëU¸ñE½utÿîë$§öÚx§ý¸ü<±›Žiyçù‚ÐLÃlu¶4Ïj¨!tT…ÉÆÁ!ºØ{Ýf»ñ A+o¯(I´Øoà/¿EéœÁ„Ï‚mÖ™€ü)_¬çS’˜çËÓ)l‰wÆ>BP£ÕÃÁb­xA>ŸÓChœËL6½h1ÝEFY/4gis3úcº0Ô“šB…‹¯íY~ ilLÿ§™0ÔŒ |¿ @˽.ÓÓ°ÓðÊAÈp¤º>­ÝJ8üYd1#pb,n£ÍÇ?ȇáË.ª2kSrúVUm±†Ñ¨6kë.±œÉ<üˆwAý~ÖkMBD´Ï·¿k•™ŸÞêdâßÐÃÖI•­é*u FbU çˆnÄÙù»Ù¹­¤²ÙfúÕ”ÏwìÓœ4 Ä­œÊSM£ï9úQ^¢T:°²º*çõTÚÜ×-–E%Šþâ°ýOÿÚ÷|6yb=VjúúÔP5€q¡Î³ÙïŒÂ,Ò½ù¡ê‚N™ÂÏv{ÓÄDÄ/ŽzÛ¡g}™2jäù<íISaìE!®«C2H$Òëdl2 rõÔ§CÏ NÖ+Û)ê·-TÎÒ766ˆÒV|»S„x¹ª¦^« -§Æ^ij?kKóœÁPG@=Ù a€#_\mÐíá!!ÐDBh¨Z~>»ý—å@®rz³„#ÃádrÈÀÍi’`”z®`´Å›¶÷m:‚Û?E6f϶ëG\,5N+fö+ùÛÏϽ˜ Ò€wY&“fç#¦ž"àý­j¾øÑ9lݱ³Mùt!®bÜmýc>Tú'Úó^åí®Ù63ÅÂÚ­mÛj¹Í¤BÝ®ÝNJ¹ì\éå‚¿ hB=BöO|HÆŽ+´ÎªE«Ý_评­r×råT*…ÃÐÙýC O5Þ0våäÕ÷™ÕEÂé~štQ(–è¨Ñí N‹(ö-Ÿ›ñeVÜ$XSå¬[5<çõS*bãÎ}ccÔxxxÏ< ððdœ]\]eôõǬ.Îc¯`ã·¶HÐLžLdpÉe¨†|Ló¸РÚzŸr8uÔŸn}]¢ÛÚÂó;VÚ@>Pwâ¾¾>"ç…®ÐÕâqü¡ÇÙÖ&êD ‰=ÀI#؆FÖô¡èÏ"S¡Q7ÐT¨a©×‚><ûé;Ÿ)çïÃp×íý©õ9KG´GAs=)BL/#úˆŠÒˆ‰‰mÚc6}jÜÎö‡gÓ£ÁŠ"KVWVf¾¬…«ç’¦eS\Ç/,"‚Ò¼/ãí}BáµÿûSIÎÉÏ'7¹‡ã½òҭߦc Ïl‰H ÿc{T‹blz½dÿ@Ñ>&‚B~ðˆ¦¥AÏ <”Öø•H9ü€Kxt±³N +´ÓS´;*³,²¡C õGцEócVR%ÂÓP²“ÛõŸ"TyØ¥F©ô?Ÿôi}.Jôrïº-â)ÅGõ›`-z£ÙžP_¸=YWÜÖ„ÎÜ”ž ±—Ðç EU 1Gܦßm3$~‘LÍ;jÞZ^a¥¦ñiâšµ­/»OÅnÒÈX7•µìÒ¦Z=Ê/Qâ²ÀÙ'ä#9ÈÅz´üÛ7¼Ð{,Ó>µ:Mœ`/&+ç “ %Ûe&¬”૲SšAÁÁ³Á§¦PŠÄïõ1 ¨7¢bª0z‘úrþ…Ðññ1|u¨°Äª?ÐgDy% ?;;sšû3Ra§O#ê¶"ðx±'•šÂÀQòáÇÒ666åååÒúúúЀQ¨9ïÒÒ’ }MÌ-5¿mÈþêp;y0}DŒê½=×~Õ³’·g0}##Ðð€¾ÒÂÂB¨bÕN; 9¦˜¤èÙ pýÞÞ^+X0¢3šÙ¶´¾ 8“ ÓL«ˆ„Mqý±µuü 4Õ ê* ¬ÞÒÒ2$''ÇÒÚ: jk¾Ð›N@Žr*•’’ÊqóUÁ‹lš1|‡hÚ`¤ôÁX jq)¹Ä×Äw¿UÕ5ˆ½¦Iç±8© Yß³‚HxóJððp½m{CV°+¸¢C2y* yŠóXº¦Ý/jîÚj.£Ÿc#Îå,Q´ÑM;&qìô§¯óá”)˜u8ºO§Ÿ2{Íégk`‰ÛÌD Ÿ EcïšèµÐî†IüÍCd{ý¹7L‡.,cÇ$…ƒ ÍÑ]ð:»ü§Ú’â ê@ViC%Ã2Týáz$5`¶lן‰XK\«6Œõr-½VÏãËL¹©‚NŒ¨ëêê6«¿”¨²J?}úš™Ö÷C=•3¥‡›€.Äï´/Ò›¬ñ 0÷¹Où"ÈÊ‘wš÷ã¡ fXóÓØøh¥L0‡æô߀4r€á.†}”S¦ =Ïð[¢]Š\F¶ngÌæÆ»²¶îU,;©­¦Ë8>-ªLéKŠËØQu¡ÿIKo!&wÐz~~eû÷·ÊÉêºy$.>ŒR 3•5üEòõð!lvvæw”e®MhÏö.Bo¬ÜúWdõ}LLªä`ibÄû¹J­ëiáE&a$OÍš “RR†*©€éå•Ó²6Ê1<¸Üè.or «çln¼ 3ÊJ¦Í’&;¯pt‘êÄ{?þîŴשM&e Ä¸vÄ{¸i&a T“û!S¼Â½ û7D¹ÈÀE– ®ÞˆÀr¤#¦„Út¿"è4#þgnÅ£÷Ç*gý;sù¿GÿûÑbg¬+Ö /(àÿ‹ý÷èÿå‘^Æè,¬Å4À rÿ™aÕøõó놰`É“PþÏ4à‡~ÿ=úïÑþ{ôߣÿý÷èÿ§G'ô§Yƒ1?“žB?>‘Q–.’2yý¿PK€|$P"0›IŠã meta.json‘AÛ …ÿJÅ9D`Àf|ÃÆVU6ê®z©zÀ6É¢Æ&²ÉªQ”ÿ^ˆÔ6­ZíªWfÞ{ß<.¨÷ãè*‘í,f':SHd70AíÀ³ÂöÆìLO³„1ŠVèhövQÓ æÐy3 */Hr®©„Óº˜7U%Í DJ€†ÔZåio2£qÛèñ.™™{“¢åPCXèZa^PŽU–¤&„R;“~ïÑu…r–çPh¯ÌÛŠb%0¯©[ y¥Ô¬2Ódgt½F)PÝf” Ü謎Ø:& Ù[.¡U5…LßIÏcçËÜ7§;/ÎO¨¤]¡ŸB}F{ ³y²ß®üa@_V©ó£ ®sΟ~È¢éñCâ|Ýùg;:3õ~pÓ~½|µ¡f)÷üb^ì€JCÍ—Òÿm6j»}|zøØÄ­~¶&¤ËýpäøÉ…¬Ó[wr¿”9åüM¤ÿ>ó÷ªþ~Cl4ùÞ-ÁÏç[“¿¦ëEêò5ÒëwPK€|$PP+A^¿ user.jsonPK€|$Pé–, k1 ædocument.jsonPK€|$PÉB)½%/|pages/844D189E-1C78-4EB7-8167-908899E0CDA6.jsonPK€|$Pô$B//† pages/91DF2135-ED2C-4ED4-9557-8F489FAC192D.jsonPK€|$PÛ¤àÅuqõ‰î previews/preview.pngPK€|$P"0›IŠã •}meta.jsonPK¥Fnode-fetch-3.3.2/docs/v2-LIMITS.md000066400000000000000000000031711445773333000163460ustar00rootroot00000000000000 Known differences ================= *As of 2.x release* - Topics such as Cross-Origin, Content Security Policy, Mixed Content, Service Workers are ignored, given our server-side context. - URL input must be an absolute URL, using either `http` or `https` as scheme. - On the upside, there are no forbidden headers. - `res.url` contains the final url when following redirects. - For convenience, `res.body` is a Node.js [Readable stream][readable-stream], so decoding can be handled independently. - Similarly, `req.body` can either be `null`, a string, a buffer or a Readable stream. - Also, you can handle rejected fetch requests through checking `err.type` and `err.code`. See [ERROR-HANDLING.md][] for more info. - Only support `res.text()`, `res.json()`, `res.blob()`, `res.arraybuffer()`, `res.buffer()` - There is currently no built-in caching, as server-side caching varies by use-cases. - Current implementation lacks server-side cookie store, you will need to extract `Set-Cookie` headers manually. - If you are using `res.clone()` and writing an isomorphic app, note that stream on Node.js have a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). - Because Node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. [readable-stream]: https://nodejs.org/api/stream.html#stream_readable_streams [ERROR-HANDLING.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md node-fetch-3.3.2/docs/v2-UPGRADE-GUIDE.md000066400000000000000000000105621445773333000172710ustar00rootroot00000000000000# Upgrade to node-fetch v2.x node-fetch v2.x brings about many changes that increase the compliance of WHATWG's [Fetch Standard][whatwg-fetch]. However, many of these changes mean that apps written for node-fetch v1.x needs to be updated to work with node-fetch v2.x and be conformant with the Fetch Standard. This document helps you make this transition. Note that this document is not an exhaustive list of all changes made in v2.x, but rather that of the most important breaking changes. See our [changelog] for other comparatively minor modifications. ## `.text()` no longer tries to detect encoding In v1.x, `response.text()` attempts to guess the text encoding of the input material and decode it for the user. However, it runs counter to the Fetch Standard which demands `.text()` to always use UTF-8. In "response" to that, we have changed `.text()` to use UTF-8. A new function **`response.textConverted()`** is created that maintains the behavior of `.text()` in v1.x. ## Internal methods hidden In v1.x, the user can access internal methods such as `_clone()`, `_decode()`, and `_convert()` on the `response` object. While these methods should never have been used, node-fetch v2.x makes these functions completely inaccessible. If your app makes use of these functions, it may break when upgrading to v2.x. If you have a use case that requires these methods to be available, feel free to file an issue and we will be happy to help you solve the problem. ## Headers The main goal we have for the `Headers` class in v2.x is to make it completely spec-compliant. These changes are done in conjunction with GitHub's [`whatwg-fetch`][gh-fetch] polyfill, [Chrome][chrome-headers], and [Firefox][firefox-headers]. ```js ////////////////////////////////////////////////////////////////////////////// // `get()` now returns **all** headers, joined by a comma, instead of only the // first one. Its original behavior can be emulated using // `get().split(',')[0]`. const headers = new Headers({ 'Abc': 'string', 'Multi': ['header1', 'header2'] }); // before after headers.get('Abc') => headers.get('Abc') => 'string' 'string' headers.get('Multi') => headers.get('Multi') => 'header1'; 'header1,header2'; headers.get('Multi').split(',')[0] => 'header1'; ////////////////////////////////////////////////////////////////////////////// // `getAll()` is removed. Its behavior in v1 can be emulated with // `get().split(',')`. const headers = new Headers({ 'Abc': 'string', 'Multi': ['header1', 'header2'] }); // before after headers.getAll('Multi') => headers.getAll('Multi') => [ 'header1', 'header2' ]; throws ReferenceError headers.get('Multi').split(',') => ['header1', 'header2']; ////////////////////////////////////////////////////////////////////////////// // All method parameters are now stringified. const headers = new Headers(); headers.set('null-header', null); headers.set('undefined', undefined); // before after headers.get('null-header') headers.get('null-header') => null => 'null' headers.get(undefined) headers.get(undefined) => throws => 'undefined' ////////////////////////////////////////////////////////////////////////////// // Invalid HTTP header names and values are now rejected outright. const headers = new Headers(); headers.set('Héy', 'ok'); // now throws headers.get('Héy'); // now throws new Headers({'Héy': 'ok'}); // now throws ``` ## Node.js v0.x support dropped If you are still using Node.js v0.10 or v0.12, upgrade ASAP. Not only has it become too much work for us to maintain, Node.js has also dropped support for those release branches in 2016. Check out Node.js' official [LTS plan] for more information on Node.js' support lifetime. [whatwg-fetch]: https://fetch.spec.whatwg.org/ [LTS plan]: https://github.com/nodejs/LTS#lts-plan [gh-fetch]: https://github.com/github/fetch [chrome-headers]: https://crbug.com/645492 [firefox-headers]: https://bugzilla.mozilla.org/show_bug.cgi?id=1278275 [changelog]: CHANGELOG.md node-fetch-3.3.2/docs/v3-LIMITS.md000066400000000000000000000032711445773333000163500ustar00rootroot00000000000000Known differences ================= *As of 3.x release* - Topics such as Cross-Origin, Content Security Policy, Mixed Content, Service Workers are ignored, given our server-side context. - On the upside, there are no forbidden headers. - `res.url` contains the final url when following redirects. - For convenience, `res.body` is a Node.js [Readable stream][readable-stream], so decoding can be handled independently. - Similarly, `req.body` can either be `null`, a buffer or a Readable stream. - Also, you can handle rejected fetch requests through checking `err.type` and `err.code`. See [ERROR-HANDLING.md][] for more info. - Only support `res.text()`, `res.json()`, `res.blob()`, `res.arraybuffer()`, `res.buffer()` - There is currently no built-in caching, as server-side caching varies by use-cases. - Current implementation lacks server-side cookie store, you will need to extract `Set-Cookie` headers manually. - If you are using `res.clone()` and writing an isomorphic app, note that stream on Node.js has a smaller internal buffer size (16Kb, aka `highWaterMark`) from client-side browsers (>1Mb, not consistent across browsers). Learn [how to get around this][highwatermark-fix]. - Because Node.js stream doesn't expose a [*disturbed*](https://fetch.spec.whatwg.org/#concept-readablestream-disturbed) property like Stream spec, using a consumed stream for `new Response(body)` will not set `bodyUsed` flag correctly. [readable-stream]: https://nodejs.org/api/stream.html#stream_readable_streams [ERROR-HANDLING.md]: https://github.com/node-fetch/node-fetch/blob/master/docs/ERROR-HANDLING.md [highwatermark-fix]: https://github.com/node-fetch/node-fetch/blob/master/README.md#custom-highwatermark node-fetch-3.3.2/docs/v3-UPGRADE-GUIDE.md000066400000000000000000000145371445773333000173000ustar00rootroot00000000000000# Upgrade to node-fetch v3.x node-fetch v3.x brings about many changes that increase the compliance of WHATWG's [Fetch Standard][whatwg-fetch]. However, many of these changes mean that apps written for node-fetch v2.x needs to be updated to work with node-fetch v3.x and be conformant with the Fetch Standard. This document helps you make this transition. Note that this document is not an exhaustive list of all changes made in v3.x, but rather that of the most important breaking changes. See our [changelog] for other comparatively minor modifications. - [Breaking Changes](#breaking) - [Enhancements](#enhancements) --- # Breaking Changes ## Minimum supported Node.js version is now 12.20 Since Node.js 10 has been deprecated since May 2020, we have decided that node-fetch v3 will drop support for Node.js 4, 6, 8, and 10 (which were previously supported). We strongly encourage you to upgrade if you still haven't done so. Check out the Node.js official [LTS plan] for more information. ## Converted to ES Module This module was converted to be a ESM only package in version `3.0.0-beta.10`. `node-fetch` is an ESM-only module - you are not able to import it with `require`. We recommend you stay on v2 which is built with CommonJS unless you use ESM yourself. We will continue to publish critical bug fixes for it. Alternatively, you can use the async `import()` function from CommonJS to load `node-fetch` asynchronously: ```js // mod.cjs const fetch = (...args) => import('node-fetch').then(({default: fetch}) => fetch(...args)); ``` ## The `timeout` option was removed. Since this was never part of the fetch specification, it was removed. AbortSignal offers more fine grained control of request timeouts, and is standardized in the Fetch spec. For convenience, you can use [timeout-signal](https://github.com/node-fetch/timeout-signal) as a workaround: ```js import timeoutSignal from 'timeout-signal'; import fetch from 'node-fetch'; const {AbortError} = fetch fetch('https://www.google.com', { signal: timeoutSignal(5000) }) .then(response => { // Handle response }) .catch(error => { if (error instanceof AbortError) { // Handle timeout } }) ``` ## `Response.statusText` no longer sets a default message derived from the HTTP status code If the server didn't respond with status text, node-fetch would set a default message derived from the HTTP status code. This behavior was not spec-compliant and now the `statusText` will remain blank instead. ## Dropped the `browser` field in package.json Prior to v3.x, we included a `browser` field in the package.json file. Since node-fetch is intended to be used on the server, we have removed this field. If you are using node-fetch client-side, consider switching to something like [cross-fetch]. ## Dropped the `res.textConverted()` function If you want charset encoding detection, please use the [fetch-charset-detection] package ([documentation][fetch-charset-detection-docs]). ```js import fetch from 'node-fetch'; import convertBody from 'fetch-charset-detection'; fetch('https://somewebsite.com').then(async res => { const buf = await res.arrayBuffer(); const text = convertBody(buf, res.headers); }); ``` ## JSON parsing errors from `res.json()` are of type `SyntaxError` instead of `FetchError` When attempting to parse invalid json via `res.json()`, a `SyntaxError` will now be thrown instead of a `FetchError` to align better with the spec. ```js import fetch from 'node-fetch'; fetch('https://somewebsitereturninginvalidjson.com').then(res => res.json()) // Throws 'Uncaught SyntaxError: Unexpected end of JSON input' or similar. ``` ## A stream pipeline is now used to forward errors If you are listening for errors via `res.body.on('error', () => ...)`, replace it with `res.body.once('error', () => ...)` so that your callback is not [fired twice](https://github.com/node-fetch/node-fetch/issues/668#issuecomment-569386115) in NodeJS >=13.5. ## `req.body` can no longer be a string We are working towards changing body to become either null or a stream. ## Changed default user agent The default user agent has been changed from `node-fetch/1.0 (+https://github.com/node-fetch/node-fetch)` to `node-fetch (+https://github.com/node-fetch/node-fetch)`. ## Arbitrary URLs are no longer supported Since in 3.x we are using the WHATWG's `new URL()`, arbitrary URL parsing will fail due to lack of base. # Enhancements ## Data URI support Previously, node-fetch only supported http url scheme. However, the Fetch Standard recently introduced the `data:` URI support. Following the specification, we implemented this feature in v3.x. Read more about `data:` URLs [here][data-url]. ## New & exposed Blob implementation Blob implementation is now [fetch-blob] and hence is exposed, unlikely previously, where Blob type was only internal and not exported. ## Better UTF-8 URL handling We now use the new Node.js [WHATWG-compliant URL API][whatwg-nodejs-url], so UTF-8 URLs are handled properly. ## Request errors are now piped using `stream.pipeline` Since the v3.x requires at least Node.js 12.20.0, we can utilise the new API. ## Creating Request/Response objects with relative URLs is no longer supported We introduced Node.js `new URL()` API in 3.x, because it offers better UTF-8 support and is WHATWG URL compatible. The drawback is, given current limit of the API (nodejs/node#12682), it's not possible to support relative URL parsing without hacks. Due to the lack of a browsing context in Node.js, we opted to drop support for relative URLs on Request/Response object, and it will now throw errors if you do so. The main `fetch()` function will support absolute URLs and data url. ## Bundled TypeScript types Since v3.x you no longer need to install `@types/node-fetch` package in order to use `node-fetch` with TypeScript. [whatwg-fetch]: https://fetch.spec.whatwg.org/ [data-url]: https://fetch.spec.whatwg.org/#data-url-processor [LTS plan]: https://github.com/nodejs/LTS#lts-plan [cross-fetch]: https://github.com/lquixada/cross-fetch [fetch-charset-detection]: https://github.com/Richienb/fetch-charset-detection [fetch-charset-detection-docs]: https://richienb.github.io/fetch-charset-detection/globals.html#convertbody [fetch-blob]: https://github.com/node-fetch/fetch-blob#readme [whatwg-nodejs-url]: https://nodejs.org/api/url.html#url_the_whatwg_url_api [changelog]: CHANGELOG.md node-fetch-3.3.2/example.js000066400000000000000000000016131445773333000155360ustar00rootroot00000000000000/* Here are some example ways in which you can use node-fetch. Test each code fragment separately so that you don't get errors related to constant reassigning, etc. Top-level `await` support is required. */ import fetch from 'node-fetch'; // Plain text or HTML const response = await fetch('https://github.com/'); const body = await response.text(); console.log(body); // JSON const response = await fetch('https://github.com/'); const json = await response.json(); console.log(json); // Simple Post const response = await fetch('https://httpbin.org/post', {method: 'POST', body: 'a=1'}); const json = await response.json(); console.log(json); // Post with JSON const body = {a: 1}; const response = await fetch('https://httpbin.org/post', { method: 'post', body: JSON.stringify(body), headers: {'Content-Type': 'application/json'} }); const json = await response.json(); console.log(json); node-fetch-3.3.2/package.json000066400000000000000000000057461445773333000160460ustar00rootroot00000000000000{ "name": "node-fetch", "version": "3.1.1", "description": "A light-weight module that brings Fetch API to node.js", "main": "./src/index.js", "sideEffects": false, "type": "module", "files": [ "src", "@types/index.d.ts" ], "types": "./@types/index.d.ts", "engines": { "node": "^12.20.0 || ^14.13.1 || >=16.0.0" }, "scripts": { "test": "mocha", "coverage": "c8 report --reporter=text-lcov | coveralls", "test-types": "tsd", "lint": "xo" }, "repository": { "type": "git", "url": "https://github.com/node-fetch/node-fetch.git" }, "keywords": [ "fetch", "http", "promise", "request", "curl", "wget", "xhr", "whatwg" ], "author": "David Frank", "license": "MIT", "bugs": { "url": "https://github.com/node-fetch/node-fetch/issues" }, "homepage": "https://github.com/node-fetch/node-fetch", "funding": { "type": "opencollective", "url": "https://opencollective.com/node-fetch" }, "devDependencies": { "abort-controller": "^3.0.0", "abortcontroller-polyfill": "^1.7.1", "busboy": "^1.4.0", "c8": "^7.7.2", "chai": "^4.3.4", "chai-as-promised": "^7.1.1", "chai-iterator": "^3.0.2", "chai-string": "^1.5.0", "coveralls": "^3.1.0", "form-data": "^4.0.0", "formdata-node": "^4.2.4", "mocha": "^9.1.3", "p-timeout": "^5.0.0", "stream-consumers": "^1.0.1", "tsd": "^0.14.0", "xo": "^0.39.1" }, "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" }, "tsd": { "cwd": "@types", "compilerOptions": { "esModuleInterop": true } }, "xo": { "envs": [ "node", "browser" ], "ignores": [ "example.js" ], "rules": { "complexity": 0, "import/extensions": 0, "import/no-useless-path-segments": 0, "import/no-anonymous-default-export": 0, "import/no-named-as-default": 0, "unicorn/import-index": 0, "unicorn/no-array-reduce": 0, "unicorn/prefer-node-protocol": 0, "unicorn/numeric-separators-style": 0, "unicorn/explicit-length-check": 0, "capitalized-comments": 0, "node/no-unsupported-features/es-syntax": 0, "@typescript-eslint/member-ordering": 0 }, "overrides": [ { "files": "test/**/*.js", "envs": [ "node", "mocha" ], "rules": { "max-nested-callbacks": 0, "no-unused-expressions": 0, "no-warning-comments": 0, "new-cap": 0, "guard-for-in": 0, "unicorn/no-array-for-each": 0, "unicorn/prevent-abbreviations": 0, "promise/prefer-await-to-then": 0, "ava/no-import-test-files": 0 } } ] }, "runkitExampleFilename": "example.js", "release": { "branches": [ "+([0-9]).x", "main", "next", { "name": "beta", "prerelease": true } ] } } node-fetch-3.3.2/src/000077500000000000000000000000001445773333000143335ustar00rootroot00000000000000node-fetch-3.3.2/src/body.js000066400000000000000000000231721445773333000156330ustar00rootroot00000000000000 /** * Body.js * * Body interface provides common methods for Request and Response */ import Stream, {PassThrough} from 'node:stream'; import {types, deprecate, promisify} from 'node:util'; import {Buffer} from 'node:buffer'; import Blob from 'fetch-blob'; import {FormData, formDataToBlob} from 'formdata-polyfill/esm.min.js'; import {FetchError} from './errors/fetch-error.js'; import {FetchBaseError} from './errors/base.js'; import {isBlob, isURLSearchParameters} from './utils/is.js'; const pipeline = promisify(Stream.pipeline); const INTERNALS = Symbol('Body internals'); /** * Body mixin * * Ref: https://fetch.spec.whatwg.org/#body * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ export default class Body { constructor(body, { size = 0 } = {}) { let boundary = null; if (body === null) { // Body is undefined or null body = null; } else if (isURLSearchParameters(body)) { // Body is a URLSearchParams body = Buffer.from(body.toString()); } else if (isBlob(body)) { // Body is blob } else if (Buffer.isBuffer(body)) { // Body is Buffer } else if (types.isAnyArrayBuffer(body)) { // Body is ArrayBuffer body = Buffer.from(body); } else if (ArrayBuffer.isView(body)) { // Body is ArrayBufferView body = Buffer.from(body.buffer, body.byteOffset, body.byteLength); } else if (body instanceof Stream) { // Body is stream } else if (body instanceof FormData) { // Body is FormData body = formDataToBlob(body); boundary = body.type.split('=')[1]; } else { // None of the above // coerce to string then buffer body = Buffer.from(String(body)); } let stream = body; if (Buffer.isBuffer(body)) { stream = Stream.Readable.from(body); } else if (isBlob(body)) { stream = Stream.Readable.from(body.stream()); } this[INTERNALS] = { body, stream, boundary, disturbed: false, error: null }; this.size = size; if (body instanceof Stream) { body.on('error', error_ => { const error = error_ instanceof FetchBaseError ? error_ : new FetchError(`Invalid response body while trying to fetch ${this.url}: ${error_.message}`, 'system', error_); this[INTERNALS].error = error; }); } } get body() { return this[INTERNALS].stream; } get bodyUsed() { return this[INTERNALS].disturbed; } /** * Decode response as ArrayBuffer * * @return Promise */ async arrayBuffer() { const {buffer, byteOffset, byteLength} = await consumeBody(this); return buffer.slice(byteOffset, byteOffset + byteLength); } async formData() { const ct = this.headers.get('content-type'); if (ct.startsWith('application/x-www-form-urlencoded')) { const formData = new FormData(); const parameters = new URLSearchParams(await this.text()); for (const [name, value] of parameters) { formData.append(name, value); } return formData; } const {toFormData} = await import('./utils/multipart-parser.js'); return toFormData(this.body, ct); } /** * Return raw response as Blob * * @return Promise */ async blob() { const ct = (this.headers && this.headers.get('content-type')) || (this[INTERNALS].body && this[INTERNALS].body.type) || ''; const buf = await this.arrayBuffer(); return new Blob([buf], { type: ct }); } /** * Decode response as json * * @return Promise */ async json() { const text = await this.text(); return JSON.parse(text); } /** * Decode response as text * * @return Promise */ async text() { const buffer = await consumeBody(this); return new TextDecoder().decode(buffer); } /** * Decode response as buffer (non-spec api) * * @return Promise */ buffer() { return consumeBody(this); } } Body.prototype.buffer = deprecate(Body.prototype.buffer, 'Please use \'response.arrayBuffer()\' instead of \'response.buffer()\'', 'node-fetch#buffer'); // In browsers, all properties are enumerable. Object.defineProperties(Body.prototype, { body: {enumerable: true}, bodyUsed: {enumerable: true}, arrayBuffer: {enumerable: true}, blob: {enumerable: true}, json: {enumerable: true}, text: {enumerable: true}, data: {get: deprecate(() => {}, 'data doesn\'t exist, use json(), text(), arrayBuffer(), or body instead', 'https://github.com/node-fetch/node-fetch/issues/1000 (response)')} }); /** * Consume and convert an entire Body to a Buffer. * * Ref: https://fetch.spec.whatwg.org/#concept-body-consume-body * * @return Promise */ async function consumeBody(data) { if (data[INTERNALS].disturbed) { throw new TypeError(`body used already for: ${data.url}`); } data[INTERNALS].disturbed = true; if (data[INTERNALS].error) { throw data[INTERNALS].error; } const {body} = data; // Body is null if (body === null) { return Buffer.alloc(0); } /* c8 ignore next 3 */ if (!(body instanceof Stream)) { return Buffer.alloc(0); } // Body is stream // get ready to actually consume the body const accum = []; let accumBytes = 0; try { for await (const chunk of body) { if (data.size > 0 && accumBytes + chunk.length > data.size) { const error = new FetchError(`content size at ${data.url} over limit: ${data.size}`, 'max-size'); body.destroy(error); throw error; } accumBytes += chunk.length; accum.push(chunk); } } catch (error) { const error_ = error instanceof FetchBaseError ? error : new FetchError(`Invalid response body while trying to fetch ${data.url}: ${error.message}`, 'system', error); throw error_; } if (body.readableEnded === true || body._readableState.ended === true) { try { if (accum.every(c => typeof c === 'string')) { return Buffer.from(accum.join('')); } return Buffer.concat(accum, accumBytes); } catch (error) { throw new FetchError(`Could not create Buffer from response body for ${data.url}: ${error.message}`, 'system', error); } } else { throw new FetchError(`Premature close of server response while trying to fetch ${data.url}`); } } /** * Clone body given Res/Req instance * * @param Mixed instance Response or Request instance * @param String highWaterMark highWaterMark for both PassThrough body streams * @return Mixed */ export const clone = (instance, highWaterMark) => { let p1; let p2; let {body} = instance[INTERNALS]; // Don't allow cloning a used body if (instance.bodyUsed) { throw new Error('cannot clone body after it is used'); } // Check that body is a stream and not form-data object // note: we can't clone the form-data object without having it as a dependency if ((body instanceof Stream) && (typeof body.getBoundary !== 'function')) { // Tee instance body p1 = new PassThrough({highWaterMark}); p2 = new PassThrough({highWaterMark}); body.pipe(p1); body.pipe(p2); // Set instance body to teed body and return the other teed body instance[INTERNALS].stream = p1; body = p2; } return body; }; const getNonSpecFormDataBoundary = deprecate( body => body.getBoundary(), 'form-data doesn\'t follow the spec and requires special treatment. Use alternative package', 'https://github.com/node-fetch/node-fetch/issues/1167' ); /** * Performs the operation "extract a `Content-Type` value from |object|" as * specified in the specification: * https://fetch.spec.whatwg.org/#concept-bodyinit-extract * * This function assumes that instance.body is present. * * @param {any} body Any options.body input * @returns {string | null} */ export const extractContentType = (body, request) => { // Body is null or undefined if (body === null) { return null; } // Body is string if (typeof body === 'string') { return 'text/plain;charset=UTF-8'; } // Body is a URLSearchParams if (isURLSearchParameters(body)) { return 'application/x-www-form-urlencoded;charset=UTF-8'; } // Body is blob if (isBlob(body)) { return body.type || null; } // Body is a Buffer (Buffer, ArrayBuffer or ArrayBufferView) if (Buffer.isBuffer(body) || types.isAnyArrayBuffer(body) || ArrayBuffer.isView(body)) { return null; } if (body instanceof FormData) { return `multipart/form-data; boundary=${request[INTERNALS].boundary}`; } // Detect form data input from form-data module if (body && typeof body.getBoundary === 'function') { return `multipart/form-data;boundary=${getNonSpecFormDataBoundary(body)}`; } // Body is stream - can't really do much about this if (body instanceof Stream) { return null; } // Body constructor defaults other things to string return 'text/plain;charset=UTF-8'; }; /** * The Fetch Standard treats this as if "total bytes" is a property on the body. * For us, we have to explicitly get it with a function. * * ref: https://fetch.spec.whatwg.org/#concept-body-total-bytes * * @param {any} obj.body Body object from the Body instance. * @returns {number | null} */ export const getTotalBytes = request => { const {body} = request[INTERNALS]; // Body is null or undefined if (body === null) { return 0; } // Body is Blob if (isBlob(body)) { return body.size; } // Body is Buffer if (Buffer.isBuffer(body)) { return body.length; } // Detect form data input from form-data module if (body && typeof body.getLengthSync === 'function') { return body.hasKnownLength && body.hasKnownLength() ? body.getLengthSync() : null; } // Body is stream return null; }; /** * Write a Body to a Node.js WritableStream (e.g. http.Request) object. * * @param {Stream.Writable} dest The stream to write to. * @param obj.body Body object from the Body instance. * @returns {Promise} */ export const writeToStream = async (dest, {body}) => { if (body === null) { // Body is null dest.end(); } else { // Body is stream await pipeline(body, dest); } }; node-fetch-3.3.2/src/errors/000077500000000000000000000000001445773333000156475ustar00rootroot00000000000000node-fetch-3.3.2/src/errors/abort-error.js000066400000000000000000000003321445773333000204410ustar00rootroot00000000000000import {FetchBaseError} from './base.js'; /** * AbortError interface for cancelled requests */ export class AbortError extends FetchBaseError { constructor(message, type = 'aborted') { super(message, type); } } node-fetch-3.3.2/src/errors/base.js000066400000000000000000000005321445773333000171170ustar00rootroot00000000000000export class FetchBaseError extends Error { constructor(message, type) { super(message); // Hide custom error implementation details from end-users Error.captureStackTrace(this, this.constructor); this.type = type; } get name() { return this.constructor.name; } get [Symbol.toStringTag]() { return this.constructor.name; } } node-fetch-3.3.2/src/errors/fetch-error.js000066400000000000000000000015471445773333000204340ustar00rootroot00000000000000 import {FetchBaseError} from './base.js'; /** * @typedef {{ address?: string, code: string, dest?: string, errno: number, info?: object, message: string, path?: string, port?: number, syscall: string}} SystemError */ /** * FetchError interface for operational errors */ export class FetchError extends FetchBaseError { /** * @param {string} message - Error message for human * @param {string} [type] - Error type for machine * @param {SystemError} [systemError] - For Node.js system error */ constructor(message, type, systemError) { super(message, type); // When err.type is `system`, err.erroredSysCall contains system error and err.code contains system error code if (systemError) { // eslint-disable-next-line no-multi-assign this.code = this.errno = systemError.code; this.erroredSysCall = systemError.syscall; } } } node-fetch-3.3.2/src/headers.js000066400000000000000000000154241445773333000163120ustar00rootroot00000000000000/** * Headers.js * * Headers class offers convenient helpers */ import {types} from 'node:util'; import http from 'node:http'; /* c8 ignore next 9 */ const validateHeaderName = typeof http.validateHeaderName === 'function' ? http.validateHeaderName : name => { if (!/^[\^`\-\w!#$%&'*+.|~]+$/.test(name)) { const error = new TypeError(`Header name must be a valid HTTP token [${name}]`); Object.defineProperty(error, 'code', {value: 'ERR_INVALID_HTTP_TOKEN'}); throw error; } }; /* c8 ignore next 9 */ const validateHeaderValue = typeof http.validateHeaderValue === 'function' ? http.validateHeaderValue : (name, value) => { if (/[^\t\u0020-\u007E\u0080-\u00FF]/.test(value)) { const error = new TypeError(`Invalid character in header content ["${name}"]`); Object.defineProperty(error, 'code', {value: 'ERR_INVALID_CHAR'}); throw error; } }; /** * @typedef {Headers | Record | Iterable | Iterable>} HeadersInit */ /** * This Fetch API interface allows you to perform various actions on HTTP request and response headers. * These actions include retrieving, setting, adding to, and removing. * A Headers object has an associated header list, which is initially empty and consists of zero or more name and value pairs. * You can add to this using methods like append() (see Examples.) * In all methods of this interface, header names are matched by case-insensitive byte sequence. * */ export default class Headers extends URLSearchParams { /** * Headers class * * @constructor * @param {HeadersInit} [init] - Response headers */ constructor(init) { // Validate and normalize init object in [name, value(s)][] /** @type {string[][]} */ let result = []; if (init instanceof Headers) { const raw = init.raw(); for (const [name, values] of Object.entries(raw)) { result.push(...values.map(value => [name, value])); } } else if (init == null) { // eslint-disable-line no-eq-null, eqeqeq // No op } else if (typeof init === 'object' && !types.isBoxedPrimitive(init)) { const method = init[Symbol.iterator]; // eslint-disable-next-line no-eq-null, eqeqeq if (method == null) { // Record result.push(...Object.entries(init)); } else { if (typeof method !== 'function') { throw new TypeError('Header pairs must be iterable'); } // Sequence> // Note: per spec we have to first exhaust the lists then process them result = [...init] .map(pair => { if ( typeof pair !== 'object' || types.isBoxedPrimitive(pair) ) { throw new TypeError('Each header pair must be an iterable object'); } return [...pair]; }).map(pair => { if (pair.length !== 2) { throw new TypeError('Each header pair must be a name/value tuple'); } return [...pair]; }); } } else { throw new TypeError('Failed to construct \'Headers\': The provided value is not of type \'(sequence> or record)'); } // Validate and lowercase result = result.length > 0 ? result.map(([name, value]) => { validateHeaderName(name); validateHeaderValue(name, String(value)); return [String(name).toLowerCase(), String(value)]; }) : undefined; super(result); // Returning a Proxy that will lowercase key names, validate parameters and sort keys // eslint-disable-next-line no-constructor-return return new Proxy(this, { get(target, p, receiver) { switch (p) { case 'append': case 'set': return (name, value) => { validateHeaderName(name); validateHeaderValue(name, String(value)); return URLSearchParams.prototype[p].call( target, String(name).toLowerCase(), String(value) ); }; case 'delete': case 'has': case 'getAll': return name => { validateHeaderName(name); return URLSearchParams.prototype[p].call( target, String(name).toLowerCase() ); }; case 'keys': return () => { target.sort(); return new Set(URLSearchParams.prototype.keys.call(target)).keys(); }; default: return Reflect.get(target, p, receiver); } } }); /* c8 ignore next */ } get [Symbol.toStringTag]() { return this.constructor.name; } toString() { return Object.prototype.toString.call(this); } get(name) { const values = this.getAll(name); if (values.length === 0) { return null; } let value = values.join(', '); if (/^content-encoding$/i.test(name)) { value = value.toLowerCase(); } return value; } forEach(callback, thisArg = undefined) { for (const name of this.keys()) { Reflect.apply(callback, thisArg, [this.get(name), name, this]); } } * values() { for (const name of this.keys()) { yield this.get(name); } } /** * @type {() => IterableIterator<[string, string]>} */ * entries() { for (const name of this.keys()) { yield [name, this.get(name)]; } } [Symbol.iterator]() { return this.entries(); } /** * Node-fetch non-spec method * returning all headers and their values as array * @returns {Record} */ raw() { return [...this.keys()].reduce((result, key) => { result[key] = this.getAll(key); return result; }, {}); } /** * For better console.log(headers) and also to convert Headers into Node.js Request compatible format */ [Symbol.for('nodejs.util.inspect.custom')]() { return [...this.keys()].reduce((result, key) => { const values = this.getAll(key); // Http.request() only supports string as Host header. // This hack makes specifying custom Host header possible. if (key === 'host') { result[key] = values[0]; } else { result[key] = values.length > 1 ? values : values[0]; } return result; }, {}); } } /** * Re-shaping object for Web IDL tests * Only need to do it for overridden methods */ Object.defineProperties( Headers.prototype, ['get', 'entries', 'forEach', 'values'].reduce((result, property) => { result[property] = {enumerable: true}; return result; }, {}) ); /** * Create a Headers object from an http.IncomingMessage.rawHeaders, ignoring those that do * not conform to HTTP grammar productions. * @param {import('http').IncomingMessage['rawHeaders']} headers */ export function fromRawHeaders(headers = []) { return new Headers( headers // Split into pairs .reduce((result, value, index, array) => { if (index % 2 === 0) { result.push(array.slice(index, index + 2)); } return result; }, []) .filter(([name, value]) => { try { validateHeaderName(name); validateHeaderValue(name, String(value)); return true; } catch { return false; } }) ); } node-fetch-3.3.2/src/index.js000066400000000000000000000311541445773333000160040ustar00rootroot00000000000000/** * Index.js * * a request API compatible with window.fetch * * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ import http from 'node:http'; import https from 'node:https'; import zlib from 'node:zlib'; import Stream, {PassThrough, pipeline as pump} from 'node:stream'; import {Buffer} from 'node:buffer'; import dataUriToBuffer from 'data-uri-to-buffer'; import {writeToStream, clone} from './body.js'; import Response from './response.js'; import Headers, {fromRawHeaders} from './headers.js'; import Request, {getNodeRequestOptions} from './request.js'; import {FetchError} from './errors/fetch-error.js'; import {AbortError} from './errors/abort-error.js'; import {isRedirect} from './utils/is-redirect.js'; import {FormData} from 'formdata-polyfill/esm.min.js'; import {isDomainOrSubdomain, isSameProtocol} from './utils/is.js'; import {parseReferrerPolicyFromHeader} from './utils/referrer.js'; import { Blob, File, fileFromSync, fileFrom, blobFromSync, blobFrom } from 'fetch-blob/from.js'; export {FormData, Headers, Request, Response, FetchError, AbortError, isRedirect}; export {Blob, File, fileFromSync, fileFrom, blobFromSync, blobFrom}; const supportedSchemas = new Set(['data:', 'http:', 'https:']); /** * Fetch function * * @param {string | URL | import('./request').default} url - Absolute url or Request instance * @param {*} [options_] - Fetch options * @return {Promise} */ export default async function fetch(url, options_) { return new Promise((resolve, reject) => { // Build request object const request = new Request(url, options_); const {parsedURL, options} = getNodeRequestOptions(request); if (!supportedSchemas.has(parsedURL.protocol)) { throw new TypeError(`node-fetch cannot load ${url}. URL scheme "${parsedURL.protocol.replace(/:$/, '')}" is not supported.`); } if (parsedURL.protocol === 'data:') { const data = dataUriToBuffer(request.url); const response = new Response(data, {headers: {'Content-Type': data.typeFull}}); resolve(response); return; } // Wrap http.request into fetch const send = (parsedURL.protocol === 'https:' ? https : http).request; const {signal} = request; let response = null; const abort = () => { const error = new AbortError('The operation was aborted.'); reject(error); if (request.body && request.body instanceof Stream.Readable) { request.body.destroy(error); } if (!response || !response.body) { return; } response.body.emit('error', error); }; if (signal && signal.aborted) { abort(); return; } const abortAndFinalize = () => { abort(); finalize(); }; // Send request const request_ = send(parsedURL.toString(), options); if (signal) { signal.addEventListener('abort', abortAndFinalize); } const finalize = () => { request_.abort(); if (signal) { signal.removeEventListener('abort', abortAndFinalize); } }; request_.on('error', error => { reject(new FetchError(`request to ${request.url} failed, reason: ${error.message}`, 'system', error)); finalize(); }); fixResponseChunkedTransferBadEnding(request_, error => { if (response && response.body) { response.body.destroy(error); } }); /* c8 ignore next 18 */ if (process.version < 'v14') { // Before Node.js 14, pipeline() does not fully support async iterators and does not always // properly handle when the socket close/end events are out of order. request_.on('socket', s => { let endedWithEventsCount; s.prependListener('end', () => { endedWithEventsCount = s._eventsCount; }); s.prependListener('close', hadError => { // if end happened before close but the socket didn't emit an error, do it now if (response && endedWithEventsCount < s._eventsCount && !hadError) { const error = new Error('Premature close'); error.code = 'ERR_STREAM_PREMATURE_CLOSE'; response.body.emit('error', error); } }); }); } request_.on('response', response_ => { request_.setTimeout(0); const headers = fromRawHeaders(response_.rawHeaders); // HTTP fetch step 5 if (isRedirect(response_.statusCode)) { // HTTP fetch step 5.2 const location = headers.get('Location'); // HTTP fetch step 5.3 let locationURL = null; try { locationURL = location === null ? null : new URL(location, request.url); } catch { // error here can only be invalid URL in Location: header // do not throw when options.redirect == manual // let the user extract the errorneous redirect URL if (request.redirect !== 'manual') { reject(new FetchError(`uri requested responds with an invalid redirect URL: ${location}`, 'invalid-redirect')); finalize(); return; } } // HTTP fetch step 5.5 switch (request.redirect) { case 'error': reject(new FetchError(`uri requested responds with a redirect, redirect mode is set to error: ${request.url}`, 'no-redirect')); finalize(); return; case 'manual': // Nothing to do break; case 'follow': { // HTTP-redirect fetch step 2 if (locationURL === null) { break; } // HTTP-redirect fetch step 5 if (request.counter >= request.follow) { reject(new FetchError(`maximum redirect reached at: ${request.url}`, 'max-redirect')); finalize(); return; } // HTTP-redirect fetch step 6 (counter increment) // Create a new Request object. const requestOptions = { headers: new Headers(request.headers), follow: request.follow, counter: request.counter + 1, agent: request.agent, compress: request.compress, method: request.method, body: clone(request), signal: request.signal, size: request.size, referrer: request.referrer, referrerPolicy: request.referrerPolicy }; // when forwarding sensitive headers like "Authorization", // "WWW-Authenticate", and "Cookie" to untrusted targets, // headers will be ignored when following a redirect to a domain // that is not a subdomain match or exact match of the initial domain. // For example, a redirect from "foo.com" to either "foo.com" or "sub.foo.com" // will forward the sensitive headers, but a redirect to "bar.com" will not. // headers will also be ignored when following a redirect to a domain using // a different protocol. For example, a redirect from "https://foo.com" to "http://foo.com" // will not forward the sensitive headers if (!isDomainOrSubdomain(request.url, locationURL) || !isSameProtocol(request.url, locationURL)) { for (const name of ['authorization', 'www-authenticate', 'cookie', 'cookie2']) { requestOptions.headers.delete(name); } } // HTTP-redirect fetch step 9 if (response_.statusCode !== 303 && request.body && options_.body instanceof Stream.Readable) { reject(new FetchError('Cannot follow redirect with body being a readable stream', 'unsupported-redirect')); finalize(); return; } // HTTP-redirect fetch step 11 if (response_.statusCode === 303 || ((response_.statusCode === 301 || response_.statusCode === 302) && request.method === 'POST')) { requestOptions.method = 'GET'; requestOptions.body = undefined; requestOptions.headers.delete('content-length'); } // HTTP-redirect fetch step 14 const responseReferrerPolicy = parseReferrerPolicyFromHeader(headers); if (responseReferrerPolicy) { requestOptions.referrerPolicy = responseReferrerPolicy; } // HTTP-redirect fetch step 15 resolve(fetch(new Request(locationURL, requestOptions))); finalize(); return; } default: return reject(new TypeError(`Redirect option '${request.redirect}' is not a valid value of RequestRedirect`)); } } // Prepare response if (signal) { response_.once('end', () => { signal.removeEventListener('abort', abortAndFinalize); }); } let body = pump(response_, new PassThrough(), error => { if (error) { reject(error); } }); // see https://github.com/nodejs/node/pull/29376 /* c8 ignore next 3 */ if (process.version < 'v12.10') { response_.on('aborted', abortAndFinalize); } const responseOptions = { url: request.url, status: response_.statusCode, statusText: response_.statusMessage, headers, size: request.size, counter: request.counter, highWaterMark: request.highWaterMark }; // HTTP-network fetch step 12.1.1.3 const codings = headers.get('Content-Encoding'); // HTTP-network fetch step 12.1.1.4: handle content codings // in following scenarios we ignore compression support // 1. compression support is disabled // 2. HEAD request // 3. no Content-Encoding header // 4. no content response (204) // 5. content not modified response (304) if (!request.compress || request.method === 'HEAD' || codings === null || response_.statusCode === 204 || response_.statusCode === 304) { response = new Response(body, responseOptions); resolve(response); return; } // For Node v6+ // Be less strict when decoding compressed responses, since sometimes // servers send slightly invalid responses that are still accepted // by common browsers. // Always using Z_SYNC_FLUSH is what cURL does. const zlibOptions = { flush: zlib.Z_SYNC_FLUSH, finishFlush: zlib.Z_SYNC_FLUSH }; // For gzip if (codings === 'gzip' || codings === 'x-gzip') { body = pump(body, zlib.createGunzip(zlibOptions), error => { if (error) { reject(error); } }); response = new Response(body, responseOptions); resolve(response); return; } // For deflate if (codings === 'deflate' || codings === 'x-deflate') { // Handle the infamous raw deflate response from old servers // a hack for old IIS and Apache servers const raw = pump(response_, new PassThrough(), error => { if (error) { reject(error); } }); raw.once('data', chunk => { // See http://stackoverflow.com/questions/37519828 if ((chunk[0] & 0x0F) === 0x08) { body = pump(body, zlib.createInflate(), error => { if (error) { reject(error); } }); } else { body = pump(body, zlib.createInflateRaw(), error => { if (error) { reject(error); } }); } response = new Response(body, responseOptions); resolve(response); }); raw.once('end', () => { // Some old IIS servers return zero-length OK deflate responses, so // 'data' is never emitted. See https://github.com/node-fetch/node-fetch/pull/903 if (!response) { response = new Response(body, responseOptions); resolve(response); } }); return; } // For br if (codings === 'br') { body = pump(body, zlib.createBrotliDecompress(), error => { if (error) { reject(error); } }); response = new Response(body, responseOptions); resolve(response); return; } // Otherwise, use response as-is response = new Response(body, responseOptions); resolve(response); }); // eslint-disable-next-line promise/prefer-await-to-then writeToStream(request_, request).catch(reject); }); } function fixResponseChunkedTransferBadEnding(request, errorCallback) { const LAST_CHUNK = Buffer.from('0\r\n\r\n'); let isChunkedTransfer = false; let properLastChunkReceived = false; let previousChunk; request.on('response', response => { const {headers} = response; isChunkedTransfer = headers['transfer-encoding'] === 'chunked' && !headers['content-length']; }); request.on('socket', socket => { const onSocketClose = () => { if (isChunkedTransfer && !properLastChunkReceived) { const error = new Error('Premature close'); error.code = 'ERR_STREAM_PREMATURE_CLOSE'; errorCallback(error); } }; const onData = buf => { properLastChunkReceived = Buffer.compare(buf.slice(-5), LAST_CHUNK) === 0; // Sometimes final 0-length chunk and end of message code are in separate packets if (!properLastChunkReceived && previousChunk) { properLastChunkReceived = ( Buffer.compare(previousChunk.slice(-3), LAST_CHUNK.slice(0, 3)) === 0 && Buffer.compare(buf.slice(-2), LAST_CHUNK.slice(3)) === 0 ); } previousChunk = buf; }; socket.prependListener('close', onSocketClose); socket.on('data', onData); request.on('close', () => { socket.removeListener('close', onSocketClose); socket.removeListener('data', onData); }); }); } node-fetch-3.3.2/src/request.js000066400000000000000000000206531445773333000163670ustar00rootroot00000000000000/** * Request.js * * Request class contains server only options * * All spec algorithm step numbers are based on https://fetch.spec.whatwg.org/commit-snapshots/ae716822cb3a61843226cd090eefc6589446c1d2/. */ import {format as formatUrl} from 'node:url'; import {deprecate} from 'node:util'; import Headers from './headers.js'; import Body, {clone, extractContentType, getTotalBytes} from './body.js'; import {isAbortSignal} from './utils/is.js'; import {getSearch} from './utils/get-search.js'; import { validateReferrerPolicy, determineRequestsReferrer, DEFAULT_REFERRER_POLICY } from './utils/referrer.js'; const INTERNALS = Symbol('Request internals'); /** * Check if `obj` is an instance of Request. * * @param {*} object * @return {boolean} */ const isRequest = object => { return ( typeof object === 'object' && typeof object[INTERNALS] === 'object' ); }; const doBadDataWarn = deprecate(() => {}, '.data is not a valid RequestInit property, use .body instead', 'https://github.com/node-fetch/node-fetch/issues/1000 (request)'); /** * Request class * * Ref: https://fetch.spec.whatwg.org/#request-class * * @param Mixed input Url or Request instance * @param Object init Custom options * @return Void */ export default class Request extends Body { constructor(input, init = {}) { let parsedURL; // Normalize input and force URL to be encoded as UTF-8 (https://github.com/node-fetch/node-fetch/issues/245) if (isRequest(input)) { parsedURL = new URL(input.url); } else { parsedURL = new URL(input); input = {}; } if (parsedURL.username !== '' || parsedURL.password !== '') { throw new TypeError(`${parsedURL} is an url with embedded credentials.`); } let method = init.method || input.method || 'GET'; if (/^(delete|get|head|options|post|put)$/i.test(method)) { method = method.toUpperCase(); } if (!isRequest(init) && 'data' in init) { doBadDataWarn(); } // eslint-disable-next-line no-eq-null, eqeqeq if ((init.body != null || (isRequest(input) && input.body !== null)) && (method === 'GET' || method === 'HEAD')) { throw new TypeError('Request with GET/HEAD method cannot have body'); } const inputBody = init.body ? init.body : (isRequest(input) && input.body !== null ? clone(input) : null); super(inputBody, { size: init.size || input.size || 0 }); const headers = new Headers(init.headers || input.headers || {}); if (inputBody !== null && !headers.has('Content-Type')) { const contentType = extractContentType(inputBody, this); if (contentType) { headers.set('Content-Type', contentType); } } let signal = isRequest(input) ? input.signal : null; if ('signal' in init) { signal = init.signal; } // eslint-disable-next-line no-eq-null, eqeqeq if (signal != null && !isAbortSignal(signal)) { throw new TypeError('Expected signal to be an instanceof AbortSignal or EventTarget'); } // §5.4, Request constructor steps, step 15.1 // eslint-disable-next-line no-eq-null, eqeqeq let referrer = init.referrer == null ? input.referrer : init.referrer; if (referrer === '') { // §5.4, Request constructor steps, step 15.2 referrer = 'no-referrer'; } else if (referrer) { // §5.4, Request constructor steps, step 15.3.1, 15.3.2 const parsedReferrer = new URL(referrer); // §5.4, Request constructor steps, step 15.3.3, 15.3.4 referrer = /^about:(\/\/)?client$/.test(parsedReferrer) ? 'client' : parsedReferrer; } else { referrer = undefined; } this[INTERNALS] = { method, redirect: init.redirect || input.redirect || 'follow', headers, parsedURL, signal, referrer }; // Node-fetch-only options this.follow = init.follow === undefined ? (input.follow === undefined ? 20 : input.follow) : init.follow; this.compress = init.compress === undefined ? (input.compress === undefined ? true : input.compress) : init.compress; this.counter = init.counter || input.counter || 0; this.agent = init.agent || input.agent; this.highWaterMark = init.highWaterMark || input.highWaterMark || 16384; this.insecureHTTPParser = init.insecureHTTPParser || input.insecureHTTPParser || false; // §5.4, Request constructor steps, step 16. // Default is empty string per https://fetch.spec.whatwg.org/#concept-request-referrer-policy this.referrerPolicy = init.referrerPolicy || input.referrerPolicy || ''; } /** @returns {string} */ get method() { return this[INTERNALS].method; } /** @returns {string} */ get url() { return formatUrl(this[INTERNALS].parsedURL); } /** @returns {Headers} */ get headers() { return this[INTERNALS].headers; } get redirect() { return this[INTERNALS].redirect; } /** @returns {AbortSignal} */ get signal() { return this[INTERNALS].signal; } // https://fetch.spec.whatwg.org/#dom-request-referrer get referrer() { if (this[INTERNALS].referrer === 'no-referrer') { return ''; } if (this[INTERNALS].referrer === 'client') { return 'about:client'; } if (this[INTERNALS].referrer) { return this[INTERNALS].referrer.toString(); } return undefined; } get referrerPolicy() { return this[INTERNALS].referrerPolicy; } set referrerPolicy(referrerPolicy) { this[INTERNALS].referrerPolicy = validateReferrerPolicy(referrerPolicy); } /** * Clone this request * * @return Request */ clone() { return new Request(this); } get [Symbol.toStringTag]() { return 'Request'; } } Object.defineProperties(Request.prototype, { method: {enumerable: true}, url: {enumerable: true}, headers: {enumerable: true}, redirect: {enumerable: true}, clone: {enumerable: true}, signal: {enumerable: true}, referrer: {enumerable: true}, referrerPolicy: {enumerable: true} }); /** * Convert a Request to Node.js http request options. * * @param {Request} request - A Request instance * @return The options object to be passed to http.request */ export const getNodeRequestOptions = request => { const {parsedURL} = request[INTERNALS]; const headers = new Headers(request[INTERNALS].headers); // Fetch step 1.3 if (!headers.has('Accept')) { headers.set('Accept', '*/*'); } // HTTP-network-or-cache fetch steps 2.4-2.7 let contentLengthValue = null; if (request.body === null && /^(post|put)$/i.test(request.method)) { contentLengthValue = '0'; } if (request.body !== null) { const totalBytes = getTotalBytes(request); // Set Content-Length if totalBytes is a number (that is not NaN) if (typeof totalBytes === 'number' && !Number.isNaN(totalBytes)) { contentLengthValue = String(totalBytes); } } if (contentLengthValue) { headers.set('Content-Length', contentLengthValue); } // 4.1. Main fetch, step 2.6 // > If request's referrer policy is the empty string, then set request's referrer policy to the // > default referrer policy. if (request.referrerPolicy === '') { request.referrerPolicy = DEFAULT_REFERRER_POLICY; } // 4.1. Main fetch, step 2.7 // > If request's referrer is not "no-referrer", set request's referrer to the result of invoking // > determine request's referrer. if (request.referrer && request.referrer !== 'no-referrer') { request[INTERNALS].referrer = determineRequestsReferrer(request); } else { request[INTERNALS].referrer = 'no-referrer'; } // 4.5. HTTP-network-or-cache fetch, step 6.9 // > If httpRequest's referrer is a URL, then append `Referer`/httpRequest's referrer, serialized // > and isomorphic encoded, to httpRequest's header list. if (request[INTERNALS].referrer instanceof URL) { headers.set('Referer', request.referrer); } // HTTP-network-or-cache fetch step 2.11 if (!headers.has('User-Agent')) { headers.set('User-Agent', 'node-fetch'); } // HTTP-network-or-cache fetch step 2.15 if (request.compress && !headers.has('Accept-Encoding')) { headers.set('Accept-Encoding', 'gzip, deflate, br'); } let {agent} = request; if (typeof agent === 'function') { agent = agent(parsedURL); } // HTTP-network fetch step 4.2 // chunked encoding is handled by Node.js const search = getSearch(parsedURL); // Pass the full URL directly to request(), but overwrite the following // options: const options = { // Overwrite search to retain trailing ? (issue #776) path: parsedURL.pathname + search, // The following options are not expressed in the URL method: request.method, headers: headers[Symbol.for('nodejs.util.inspect.custom')](), insecureHTTPParser: request.insecureHTTPParser, agent }; return { /** @type {URL} */ parsedURL, options }; }; node-fetch-3.3.2/src/response.js000066400000000000000000000065511445773333000165360ustar00rootroot00000000000000/** * Response.js * * Response class provides content decoding */ import Headers from './headers.js'; import Body, {clone, extractContentType} from './body.js'; import {isRedirect} from './utils/is-redirect.js'; const INTERNALS = Symbol('Response internals'); /** * Response class * * Ref: https://fetch.spec.whatwg.org/#response-class * * @param Stream body Readable stream * @param Object opts Response options * @return Void */ export default class Response extends Body { constructor(body = null, options = {}) { super(body, options); // eslint-disable-next-line no-eq-null, eqeqeq, no-negated-condition const status = options.status != null ? options.status : 200; const headers = new Headers(options.headers); if (body !== null && !headers.has('Content-Type')) { const contentType = extractContentType(body, this); if (contentType) { headers.append('Content-Type', contentType); } } this[INTERNALS] = { type: 'default', url: options.url, status, statusText: options.statusText || '', headers, counter: options.counter, highWaterMark: options.highWaterMark }; } get type() { return this[INTERNALS].type; } get url() { return this[INTERNALS].url || ''; } get status() { return this[INTERNALS].status; } /** * Convenience property representing if the request ended normally */ get ok() { return this[INTERNALS].status >= 200 && this[INTERNALS].status < 300; } get redirected() { return this[INTERNALS].counter > 0; } get statusText() { return this[INTERNALS].statusText; } get headers() { return this[INTERNALS].headers; } get highWaterMark() { return this[INTERNALS].highWaterMark; } /** * Clone this response * * @return Response */ clone() { return new Response(clone(this, this.highWaterMark), { type: this.type, url: this.url, status: this.status, statusText: this.statusText, headers: this.headers, ok: this.ok, redirected: this.redirected, size: this.size, highWaterMark: this.highWaterMark }); } /** * @param {string} url The URL that the new response is to originate from. * @param {number} status An optional status code for the response (e.g., 302.) * @returns {Response} A Response object. */ static redirect(url, status = 302) { if (!isRedirect(status)) { throw new RangeError('Failed to execute "redirect" on "response": Invalid status code'); } return new Response(null, { headers: { location: new URL(url).toString() }, status }); } static error() { const response = new Response(null, {status: 0, statusText: ''}); response[INTERNALS].type = 'error'; return response; } static json(data = undefined, init = {}) { const body = JSON.stringify(data); if (body === undefined) { throw new TypeError('data is not JSON serializable'); } const headers = new Headers(init && init.headers); if (!headers.has('content-type')) { headers.set('content-type', 'application/json'); } return new Response(body, { ...init, headers }); } get [Symbol.toStringTag]() { return 'Response'; } } Object.defineProperties(Response.prototype, { type: {enumerable: true}, url: {enumerable: true}, status: {enumerable: true}, ok: {enumerable: true}, redirected: {enumerable: true}, statusText: {enumerable: true}, headers: {enumerable: true}, clone: {enumerable: true} }); node-fetch-3.3.2/src/utils/000077500000000000000000000000001445773333000154735ustar00rootroot00000000000000node-fetch-3.3.2/src/utils/get-search.js000066400000000000000000000004501445773333000200520ustar00rootroot00000000000000export const getSearch = parsedURL => { if (parsedURL.search) { return parsedURL.search; } const lastOffset = parsedURL.href.length - 1; const hash = parsedURL.hash || (parsedURL.href[lastOffset] === '#' ? '#' : ''); return parsedURL.href[lastOffset - hash.length] === '?' ? '?' : ''; }; node-fetch-3.3.2/src/utils/is-redirect.js000066400000000000000000000003451445773333000202450ustar00rootroot00000000000000const redirectStatus = new Set([301, 302, 303, 307, 308]); /** * Redirect code matching * * @param {number} code - Status code * @return {boolean} */ export const isRedirect = code => { return redirectStatus.has(code); }; node-fetch-3.3.2/src/utils/is.js000066400000000000000000000043061445773333000164470ustar00rootroot00000000000000/** * Is.js * * Object type checks. */ const NAME = Symbol.toStringTag; /** * Check if `obj` is a URLSearchParams object * ref: https://github.com/node-fetch/node-fetch/issues/296#issuecomment-307598143 * @param {*} object - Object to check for * @return {boolean} */ export const isURLSearchParameters = object => { return ( typeof object === 'object' && typeof object.append === 'function' && typeof object.delete === 'function' && typeof object.get === 'function' && typeof object.getAll === 'function' && typeof object.has === 'function' && typeof object.set === 'function' && typeof object.sort === 'function' && object[NAME] === 'URLSearchParams' ); }; /** * Check if `object` is a W3C `Blob` object (which `File` inherits from) * @param {*} object - Object to check for * @return {boolean} */ export const isBlob = object => { return ( object && typeof object === 'object' && typeof object.arrayBuffer === 'function' && typeof object.type === 'string' && typeof object.stream === 'function' && typeof object.constructor === 'function' && /^(Blob|File)$/.test(object[NAME]) ); }; /** * Check if `obj` is an instance of AbortSignal. * @param {*} object - Object to check for * @return {boolean} */ export const isAbortSignal = object => { return ( typeof object === 'object' && ( object[NAME] === 'AbortSignal' || object[NAME] === 'EventTarget' ) ); }; /** * isDomainOrSubdomain reports whether sub is a subdomain (or exact match) of * the parent domain. * * Both domains must already be in canonical form. * @param {string|URL} original * @param {string|URL} destination */ export const isDomainOrSubdomain = (destination, original) => { const orig = new URL(original).hostname; const dest = new URL(destination).hostname; return orig === dest || orig.endsWith(`.${dest}`); }; /** * isSameProtocol reports whether the two provided URLs use the same protocol. * * Both domains must already be in canonical form. * @param {string|URL} original * @param {string|URL} destination */ export const isSameProtocol = (destination, original) => { const orig = new URL(original).protocol; const dest = new URL(destination).protocol; return orig === dest; }; node-fetch-3.3.2/src/utils/multipart-parser.js000066400000000000000000000226531445773333000213540ustar00rootroot00000000000000import {File} from 'fetch-blob/from.js'; import {FormData} from 'formdata-polyfill/esm.min.js'; let s = 0; const S = { START_BOUNDARY: s++, HEADER_FIELD_START: s++, HEADER_FIELD: s++, HEADER_VALUE_START: s++, HEADER_VALUE: s++, HEADER_VALUE_ALMOST_DONE: s++, HEADERS_ALMOST_DONE: s++, PART_DATA_START: s++, PART_DATA: s++, END: s++ }; let f = 1; const F = { PART_BOUNDARY: f, LAST_BOUNDARY: f *= 2 }; const LF = 10; const CR = 13; const SPACE = 32; const HYPHEN = 45; const COLON = 58; const A = 97; const Z = 122; const lower = c => c | 0x20; const noop = () => {}; class MultipartParser { /** * @param {string} boundary */ constructor(boundary) { this.index = 0; this.flags = 0; this.onHeaderEnd = noop; this.onHeaderField = noop; this.onHeadersEnd = noop; this.onHeaderValue = noop; this.onPartBegin = noop; this.onPartData = noop; this.onPartEnd = noop; this.boundaryChars = {}; boundary = '\r\n--' + boundary; const ui8a = new Uint8Array(boundary.length); for (let i = 0; i < boundary.length; i++) { ui8a[i] = boundary.charCodeAt(i); this.boundaryChars[ui8a[i]] = true; } this.boundary = ui8a; this.lookbehind = new Uint8Array(this.boundary.length + 8); this.state = S.START_BOUNDARY; } /** * @param {Uint8Array} data */ write(data) { let i = 0; const length_ = data.length; let previousIndex = this.index; let {lookbehind, boundary, boundaryChars, index, state, flags} = this; const boundaryLength = this.boundary.length; const boundaryEnd = boundaryLength - 1; const bufferLength = data.length; let c; let cl; const mark = name => { this[name + 'Mark'] = i; }; const clear = name => { delete this[name + 'Mark']; }; const callback = (callbackSymbol, start, end, ui8a) => { if (start === undefined || start !== end) { this[callbackSymbol](ui8a && ui8a.subarray(start, end)); } }; const dataCallback = (name, clear) => { const markSymbol = name + 'Mark'; if (!(markSymbol in this)) { return; } if (clear) { callback(name, this[markSymbol], i, data); delete this[markSymbol]; } else { callback(name, this[markSymbol], data.length, data); this[markSymbol] = 0; } }; for (i = 0; i < length_; i++) { c = data[i]; switch (state) { case S.START_BOUNDARY: if (index === boundary.length - 2) { if (c === HYPHEN) { flags |= F.LAST_BOUNDARY; } else if (c !== CR) { return; } index++; break; } else if (index - 1 === boundary.length - 2) { if (flags & F.LAST_BOUNDARY && c === HYPHEN) { state = S.END; flags = 0; } else if (!(flags & F.LAST_BOUNDARY) && c === LF) { index = 0; callback('onPartBegin'); state = S.HEADER_FIELD_START; } else { return; } break; } if (c !== boundary[index + 2]) { index = -2; } if (c === boundary[index + 2]) { index++; } break; case S.HEADER_FIELD_START: state = S.HEADER_FIELD; mark('onHeaderField'); index = 0; // falls through case S.HEADER_FIELD: if (c === CR) { clear('onHeaderField'); state = S.HEADERS_ALMOST_DONE; break; } index++; if (c === HYPHEN) { break; } if (c === COLON) { if (index === 1) { // empty header field return; } dataCallback('onHeaderField', true); state = S.HEADER_VALUE_START; break; } cl = lower(c); if (cl < A || cl > Z) { return; } break; case S.HEADER_VALUE_START: if (c === SPACE) { break; } mark('onHeaderValue'); state = S.HEADER_VALUE; // falls through case S.HEADER_VALUE: if (c === CR) { dataCallback('onHeaderValue', true); callback('onHeaderEnd'); state = S.HEADER_VALUE_ALMOST_DONE; } break; case S.HEADER_VALUE_ALMOST_DONE: if (c !== LF) { return; } state = S.HEADER_FIELD_START; break; case S.HEADERS_ALMOST_DONE: if (c !== LF) { return; } callback('onHeadersEnd'); state = S.PART_DATA_START; break; case S.PART_DATA_START: state = S.PART_DATA; mark('onPartData'); // falls through case S.PART_DATA: previousIndex = index; if (index === 0) { // boyer-moore derrived algorithm to safely skip non-boundary data i += boundaryEnd; while (i < bufferLength && !(data[i] in boundaryChars)) { i += boundaryLength; } i -= boundaryEnd; c = data[i]; } if (index < boundary.length) { if (boundary[index] === c) { if (index === 0) { dataCallback('onPartData', true); } index++; } else { index = 0; } } else if (index === boundary.length) { index++; if (c === CR) { // CR = part boundary flags |= F.PART_BOUNDARY; } else if (c === HYPHEN) { // HYPHEN = end boundary flags |= F.LAST_BOUNDARY; } else { index = 0; } } else if (index - 1 === boundary.length) { if (flags & F.PART_BOUNDARY) { index = 0; if (c === LF) { // unset the PART_BOUNDARY flag flags &= ~F.PART_BOUNDARY; callback('onPartEnd'); callback('onPartBegin'); state = S.HEADER_FIELD_START; break; } } else if (flags & F.LAST_BOUNDARY) { if (c === HYPHEN) { callback('onPartEnd'); state = S.END; flags = 0; } else { index = 0; } } else { index = 0; } } if (index > 0) { // when matching a possible boundary, keep a lookbehind reference // in case it turns out to be a false lead lookbehind[index - 1] = c; } else if (previousIndex > 0) { // if our boundary turned out to be rubbish, the captured lookbehind // belongs to partData const _lookbehind = new Uint8Array(lookbehind.buffer, lookbehind.byteOffset, lookbehind.byteLength); callback('onPartData', 0, previousIndex, _lookbehind); previousIndex = 0; mark('onPartData'); // reconsider the current character even so it interrupted the sequence // it could be the beginning of a new sequence i--; } break; case S.END: break; default: throw new Error(`Unexpected state entered: ${state}`); } } dataCallback('onHeaderField'); dataCallback('onHeaderValue'); dataCallback('onPartData'); // Update properties for the next call this.index = index; this.state = state; this.flags = flags; } end() { if ((this.state === S.HEADER_FIELD_START && this.index === 0) || (this.state === S.PART_DATA && this.index === this.boundary.length)) { this.onPartEnd(); } else if (this.state !== S.END) { throw new Error('MultipartParser.end(): stream ended unexpectedly'); } } } function _fileName(headerValue) { // matches either a quoted-string or a token (RFC 2616 section 19.5.1) const m = headerValue.match(/\bfilename=("(.*?)"|([^()<>@,;:\\"/[\]?={}\s\t]+))($|;\s)/i); if (!m) { return; } const match = m[2] || m[3] || ''; let filename = match.slice(match.lastIndexOf('\\') + 1); filename = filename.replace(/%22/g, '"'); filename = filename.replace(/&#(\d{4});/g, (m, code) => { return String.fromCharCode(code); }); return filename; } export async function toFormData(Body, ct) { if (!/multipart/i.test(ct)) { throw new TypeError('Failed to fetch'); } const m = ct.match(/boundary=(?:"([^"]+)"|([^;]+))/i); if (!m) { throw new TypeError('no or bad content-type header, no multipart boundary'); } const parser = new MultipartParser(m[1] || m[2]); let headerField; let headerValue; let entryValue; let entryName; let contentType; let filename; const entryChunks = []; const formData = new FormData(); const onPartData = ui8a => { entryValue += decoder.decode(ui8a, {stream: true}); }; const appendToFile = ui8a => { entryChunks.push(ui8a); }; const appendFileToFormData = () => { const file = new File(entryChunks, filename, {type: contentType}); formData.append(entryName, file); }; const appendEntryToFormData = () => { formData.append(entryName, entryValue); }; const decoder = new TextDecoder('utf-8'); decoder.decode(); parser.onPartBegin = function () { parser.onPartData = onPartData; parser.onPartEnd = appendEntryToFormData; headerField = ''; headerValue = ''; entryValue = ''; entryName = ''; contentType = ''; filename = null; entryChunks.length = 0; }; parser.onHeaderField = function (ui8a) { headerField += decoder.decode(ui8a, {stream: true}); }; parser.onHeaderValue = function (ui8a) { headerValue += decoder.decode(ui8a, {stream: true}); }; parser.onHeaderEnd = function () { headerValue += decoder.decode(); headerField = headerField.toLowerCase(); if (headerField === 'content-disposition') { // matches either a quoted-string or a token (RFC 2616 section 19.5.1) const m = headerValue.match(/\bname=("([^"]*)"|([^()<>@,;:\\"/[\]?={}\s\t]+))/i); if (m) { entryName = m[2] || m[3] || ''; } filename = _fileName(headerValue); if (filename) { parser.onPartData = appendToFile; parser.onPartEnd = appendFileToFormData; } } else if (headerField === 'content-type') { contentType = headerValue; } headerValue = ''; headerField = ''; }; for await (const chunk of Body) { parser.write(chunk); } parser.end(); return formData; } node-fetch-3.3.2/src/utils/referrer.js000066400000000000000000000271051445773333000176520ustar00rootroot00000000000000import {isIP} from 'node:net'; /** * @external URL * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/URL|URL} */ /** * @module utils/referrer * @private */ /** * @see {@link https://w3c.github.io/webappsec-referrer-policy/#strip-url|Referrer Policy §8.4. Strip url for use as a referrer} * @param {string} URL * @param {boolean} [originOnly=false] */ export function stripURLForUseAsAReferrer(url, originOnly = false) { // 1. If url is null, return no referrer. if (url == null) { // eslint-disable-line no-eq-null, eqeqeq return 'no-referrer'; } url = new URL(url); // 2. If url's scheme is a local scheme, then return no referrer. if (/^(about|blob|data):$/.test(url.protocol)) { return 'no-referrer'; } // 3. Set url's username to the empty string. url.username = ''; // 4. Set url's password to null. // Note: `null` appears to be a mistake as this actually results in the password being `"null"`. url.password = ''; // 5. Set url's fragment to null. // Note: `null` appears to be a mistake as this actually results in the fragment being `"#null"`. url.hash = ''; // 6. If the origin-only flag is true, then: if (originOnly) { // 6.1. Set url's path to null. // Note: `null` appears to be a mistake as this actually results in the path being `"/null"`. url.pathname = ''; // 6.2. Set url's query to null. // Note: `null` appears to be a mistake as this actually results in the query being `"?null"`. url.search = ''; } // 7. Return url. return url; } /** * @see {@link https://w3c.github.io/webappsec-referrer-policy/#enumdef-referrerpolicy|enum ReferrerPolicy} */ export const ReferrerPolicy = new Set([ '', 'no-referrer', 'no-referrer-when-downgrade', 'same-origin', 'origin', 'strict-origin', 'origin-when-cross-origin', 'strict-origin-when-cross-origin', 'unsafe-url' ]); /** * @see {@link https://w3c.github.io/webappsec-referrer-policy/#default-referrer-policy|default referrer policy} */ export const DEFAULT_REFERRER_POLICY = 'strict-origin-when-cross-origin'; /** * @see {@link https://w3c.github.io/webappsec-referrer-policy/#referrer-policies|Referrer Policy §3. Referrer Policies} * @param {string} referrerPolicy * @returns {string} referrerPolicy */ export function validateReferrerPolicy(referrerPolicy) { if (!ReferrerPolicy.has(referrerPolicy)) { throw new TypeError(`Invalid referrerPolicy: ${referrerPolicy}`); } return referrerPolicy; } /** * @see {@link https://w3c.github.io/webappsec-secure-contexts/#is-origin-trustworthy|Referrer Policy §3.2. Is origin potentially trustworthy?} * @param {external:URL} url * @returns `true`: "Potentially Trustworthy", `false`: "Not Trustworthy" */ export function isOriginPotentiallyTrustworthy(url) { // 1. If origin is an opaque origin, return "Not Trustworthy". // Not applicable // 2. Assert: origin is a tuple origin. // Not for implementations // 3. If origin's scheme is either "https" or "wss", return "Potentially Trustworthy". if (/^(http|ws)s:$/.test(url.protocol)) { return true; } // 4. If origin's host component matches one of the CIDR notations 127.0.0.0/8 or ::1/128 [RFC4632], return "Potentially Trustworthy". const hostIp = url.host.replace(/(^\[)|(]$)/g, ''); const hostIPVersion = isIP(hostIp); if (hostIPVersion === 4 && /^127\./.test(hostIp)) { return true; } if (hostIPVersion === 6 && /^(((0+:){7})|(::(0+:){0,6}))0*1$/.test(hostIp)) { return true; } // 5. If origin's host component is "localhost" or falls within ".localhost", and the user agent conforms to the name resolution rules in [let-localhost-be-localhost], return "Potentially Trustworthy". // We are returning FALSE here because we cannot ensure conformance to // let-localhost-be-loalhost (https://tools.ietf.org/html/draft-west-let-localhost-be-localhost) if (url.host === 'localhost' || url.host.endsWith('.localhost')) { return false; } // 6. If origin's scheme component is file, return "Potentially Trustworthy". if (url.protocol === 'file:') { return true; } // 7. If origin's scheme component is one which the user agent considers to be authenticated, return "Potentially Trustworthy". // Not supported // 8. If origin has been configured as a trustworthy origin, return "Potentially Trustworthy". // Not supported // 9. Return "Not Trustworthy". return false; } /** * @see {@link https://w3c.github.io/webappsec-secure-contexts/#is-url-trustworthy|Referrer Policy §3.3. Is url potentially trustworthy?} * @param {external:URL} url * @returns `true`: "Potentially Trustworthy", `false`: "Not Trustworthy" */ export function isUrlPotentiallyTrustworthy(url) { // 1. If url is "about:blank" or "about:srcdoc", return "Potentially Trustworthy". if (/^about:(blank|srcdoc)$/.test(url)) { return true; } // 2. If url's scheme is "data", return "Potentially Trustworthy". if (url.protocol === 'data:') { return true; } // Note: The origin of blob: and filesystem: URLs is the origin of the context in which they were // created. Therefore, blobs created in a trustworthy origin will themselves be potentially // trustworthy. if (/^(blob|filesystem):$/.test(url.protocol)) { return true; } // 3. Return the result of executing §3.2 Is origin potentially trustworthy? on url's origin. return isOriginPotentiallyTrustworthy(url); } /** * Modifies the referrerURL to enforce any extra security policy considerations. * @see {@link https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer|Referrer Policy §8.3. Determine request's Referrer}, step 7 * @callback module:utils/referrer~referrerURLCallback * @param {external:URL} referrerURL * @returns {external:URL} modified referrerURL */ /** * Modifies the referrerOrigin to enforce any extra security policy considerations. * @see {@link https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer|Referrer Policy §8.3. Determine request's Referrer}, step 7 * @callback module:utils/referrer~referrerOriginCallback * @param {external:URL} referrerOrigin * @returns {external:URL} modified referrerOrigin */ /** * @see {@link https://w3c.github.io/webappsec-referrer-policy/#determine-requests-referrer|Referrer Policy §8.3. Determine request's Referrer} * @param {Request} request * @param {object} o * @param {module:utils/referrer~referrerURLCallback} o.referrerURLCallback * @param {module:utils/referrer~referrerOriginCallback} o.referrerOriginCallback * @returns {external:URL} Request's referrer */ export function determineRequestsReferrer(request, {referrerURLCallback, referrerOriginCallback} = {}) { // There are 2 notes in the specification about invalid pre-conditions. We return null, here, for // these cases: // > Note: If request's referrer is "no-referrer", Fetch will not call into this algorithm. // > Note: If request's referrer policy is the empty string, Fetch will not call into this // > algorithm. if (request.referrer === 'no-referrer' || request.referrerPolicy === '') { return null; } // 1. Let policy be request's associated referrer policy. const policy = request.referrerPolicy; // 2. Let environment be request's client. // not applicable to node.js // 3. Switch on request's referrer: if (request.referrer === 'about:client') { return 'no-referrer'; } // "a URL": Let referrerSource be request's referrer. const referrerSource = request.referrer; // 4. Let request's referrerURL be the result of stripping referrerSource for use as a referrer. let referrerURL = stripURLForUseAsAReferrer(referrerSource); // 5. Let referrerOrigin be the result of stripping referrerSource for use as a referrer, with the // origin-only flag set to true. let referrerOrigin = stripURLForUseAsAReferrer(referrerSource, true); // 6. If the result of serializing referrerURL is a string whose length is greater than 4096, set // referrerURL to referrerOrigin. if (referrerURL.toString().length > 4096) { referrerURL = referrerOrigin; } // 7. The user agent MAY alter referrerURL or referrerOrigin at this point to enforce arbitrary // policy considerations in the interests of minimizing data leakage. For example, the user // agent could strip the URL down to an origin, modify its host, replace it with an empty // string, etc. if (referrerURLCallback) { referrerURL = referrerURLCallback(referrerURL); } if (referrerOriginCallback) { referrerOrigin = referrerOriginCallback(referrerOrigin); } // 8.Execute the statements corresponding to the value of policy: const currentURL = new URL(request.url); switch (policy) { case 'no-referrer': return 'no-referrer'; case 'origin': return referrerOrigin; case 'unsafe-url': return referrerURL; case 'strict-origin': // 1. If referrerURL is a potentially trustworthy URL and request's current URL is not a // potentially trustworthy URL, then return no referrer. if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) { return 'no-referrer'; } // 2. Return referrerOrigin. return referrerOrigin.toString(); case 'strict-origin-when-cross-origin': // 1. If the origin of referrerURL and the origin of request's current URL are the same, then // return referrerURL. if (referrerURL.origin === currentURL.origin) { return referrerURL; } // 2. If referrerURL is a potentially trustworthy URL and request's current URL is not a // potentially trustworthy URL, then return no referrer. if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) { return 'no-referrer'; } // 3. Return referrerOrigin. return referrerOrigin; case 'same-origin': // 1. If the origin of referrerURL and the origin of request's current URL are the same, then // return referrerURL. if (referrerURL.origin === currentURL.origin) { return referrerURL; } // 2. Return no referrer. return 'no-referrer'; case 'origin-when-cross-origin': // 1. If the origin of referrerURL and the origin of request's current URL are the same, then // return referrerURL. if (referrerURL.origin === currentURL.origin) { return referrerURL; } // Return referrerOrigin. return referrerOrigin; case 'no-referrer-when-downgrade': // 1. If referrerURL is a potentially trustworthy URL and request's current URL is not a // potentially trustworthy URL, then return no referrer. if (isUrlPotentiallyTrustworthy(referrerURL) && !isUrlPotentiallyTrustworthy(currentURL)) { return 'no-referrer'; } // 2. Return referrerURL. return referrerURL; default: throw new TypeError(`Invalid referrerPolicy: ${policy}`); } } /** * @see {@link https://w3c.github.io/webappsec-referrer-policy/#parse-referrer-policy-from-header|Referrer Policy §8.1. Parse a referrer policy from a Referrer-Policy header} * @param {Headers} headers Response headers * @returns {string} policy */ export function parseReferrerPolicyFromHeader(headers) { // 1. Let policy-tokens be the result of extracting header list values given `Referrer-Policy` // and response’s header list. const policyTokens = (headers.get('referrer-policy') || '').split(/[,\s]+/); // 2. Let policy be the empty string. let policy = ''; // 3. For each token in policy-tokens, if token is a referrer policy and token is not the empty // string, then set policy to token. // Note: This algorithm loops over multiple policy values to allow deployment of new policy // values with fallbacks for older user agents, as described in § 11.1 Unknown Policy Values. for (const token of policyTokens) { if (token && ReferrerPolicy.has(token)) { policy = token; } } // 4. Return policy. return policy; } node-fetch-3.3.2/test/000077500000000000000000000000001445773333000145235ustar00rootroot00000000000000node-fetch-3.3.2/test/external-encoding.js000066400000000000000000000026511445773333000204730ustar00rootroot00000000000000import chai from 'chai'; import fetch from '../src/index.js'; const {expect} = chai; describe('external encoding', () => { describe('data uri', () => { it('should accept base64-encoded gif data uri', async () => { const b64 = 'data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs='; const res = await fetch(b64); expect(res.status).to.equal(200); expect(res.headers.get('Content-Type')).to.equal('image/gif'); const buf = await res.arrayBuffer(); expect(buf.byteLength).to.equal(35); expect(buf).to.be.an.instanceOf(ArrayBuffer); }); it('should accept data uri with specified charset', async () => { const r = await fetch('data:text/plain;charset=UTF-8;page=21,the%20data:1234,5678'); expect(r.status).to.equal(200); expect(r.headers.get('Content-Type')).to.equal('text/plain;charset=UTF-8;page=21'); const b = await r.text(); expect(b).to.equal('the data:1234,5678'); }); it('should accept data uri of plain text', () => { return fetch('data:,Hello%20World!').then(r => { expect(r.status).to.equal(200); expect(r.headers.get('Content-Type')).to.equal('text/plain;charset=US-ASCII'); return r.text().then(t => expect(t).to.equal('Hello World!')); }); }); it('should reject invalid data uri', () => { return fetch('data:@@@@').catch(error => { expect(error).to.exist; expect(error.message).to.include('malformed data: URI'); }); }); }); }); node-fetch-3.3.2/test/form-data.js000066400000000000000000000052061445773333000167360ustar00rootroot00000000000000import {FormData as FormDataNode} from 'formdata-node'; import chai from 'chai'; import {Request, Response, FormData, Blob} from '../src/index.js'; const {expect} = chai; describe('FormData', () => { it('Consume empty URLSearchParams as FormData', async () => { const res = new Response(new URLSearchParams()); const fd = await res.formData(); expect(fd).to.be.instanceOf(FormData); }); it('Consume empty URLSearchParams as FormData', async () => { const req = new Request('about:blank', { method: 'POST', body: new URLSearchParams() }); const fd = await req.formData(); expect(fd).to.be.instanceOf(FormData); }); it('Consume empty response.formData() as FormData', async () => { const res = new Response(new FormData()); const fd = await res.formData(); expect(fd).to.be.instanceOf(FormData); }); it('Consume empty response.formData() as FormData', async () => { const res = new Response(new FormData()); const fd = await res.formData(); expect(fd).to.be.instanceOf(FormData); }); it('Consume empty request.formData() as FormData', async () => { const req = new Request('about:blank', { method: 'POST', body: new FormData() }); const fd = await req.formData(); expect(fd).to.be.instanceOf(FormData); }); it('Consume URLSearchParams with entries as FormData', async () => { const res = new Response(new URLSearchParams({foo: 'bar'})); const fd = await res.formData(); expect(fd.get('foo')).to.be.equal('bar'); }); it('should return a length for empty form-data', async () => { const form = new FormData(); const ab = await new Request('http://a', { method: 'post', body: form }).arrayBuffer(); expect(ab.byteLength).to.be.greaterThan(30); }); it('should add a Blob field\'s size to the FormData length', async () => { const form = new FormData(); const string = 'Hello, world!'; form.set('field', string); const fd = await new Request('about:blank', {method: 'POST', body: form}).formData(); expect(fd.get('field')).to.equal(string); }); it('should return a length for a Blob field', async () => { const form = new FormData(); const blob = new Blob(['Hello, world!'], {type: 'text/plain'}); form.set('blob', blob); const fd = await new Response(form).formData(); expect(fd.get('blob').size).to.equal(13); }); it('FormData-node still works thanks to symbol.hasInstance', async () => { const form = new FormDataNode(); form.append('file', new Blob(['abc'], {type: 'text/html'})); const res = new Response(form); const fd = await res.formData(); expect(await fd.get('file').text()).to.equal('abc'); expect(fd.get('file').type).to.equal('text/html'); }); }); node-fetch-3.3.2/test/headers.js000066400000000000000000000156241445773333000165040ustar00rootroot00000000000000import {format} from 'node:util'; import chai from 'chai'; import chaiIterator from 'chai-iterator'; import {Headers} from '../src/index.js'; chai.use(chaiIterator); const {expect} = chai; describe('Headers', () => { it('should have attributes conforming to Web IDL', () => { const headers = new Headers(); expect(Object.getOwnPropertyNames(headers)).to.be.empty; const enumerableProperties = []; for (const property in headers) { enumerableProperties.push(property); } for (const toCheck of [ 'append', 'delete', 'entries', 'forEach', 'get', 'has', 'keys', 'set', 'values' ]) { expect(enumerableProperties).to.contain(toCheck); } }); it('should allow iterating through all headers with forEach', () => { const headers = new Headers([ ['b', '2'], ['c', '4'], ['b', '3'], ['a', '1'] ]); expect(headers).to.have.property('forEach'); const result = []; for (const [key, value] of headers.entries()) { result.push([key, value]); } expect(result).to.deep.equal([ ['a', '1'], ['b', '2, 3'], ['c', '4'] ]); }); it('should be iterable with forEach', () => { const headers = new Headers(); headers.append('Accept', 'application/json'); headers.append('Accept', 'text/plain'); headers.append('Content-Type', 'text/html'); const results = []; headers.forEach((value, key, object) => { results.push({value, key, object}); }); expect(results.length).to.equal(2); expect({key: 'accept', value: 'application/json, text/plain', object: headers}).to.deep.equal(results[0]); expect({key: 'content-type', value: 'text/html', object: headers}).to.deep.equal(results[1]); }); it('should set "this" to undefined by default on forEach', () => { const headers = new Headers({Accept: 'application/json'}); headers.forEach(function () { expect(this).to.be.undefined; }); }); it('should accept thisArg as a second argument for forEach', () => { const headers = new Headers({Accept: 'application/json'}); const thisArg = {}; headers.forEach(function () { expect(this).to.equal(thisArg); }, thisArg); }); it('should allow iterating through all headers with for-of loop', () => { const headers = new Headers([ ['b', '2'], ['c', '4'], ['a', '1'] ]); headers.append('b', '3'); expect(headers).to.be.iterable; const result = []; for (const pair of headers) { result.push(pair); } expect(result).to.deep.equal([ ['a', '1'], ['b', '2, 3'], ['c', '4'] ]); }); it('should allow iterating through all headers with entries()', () => { const headers = new Headers([ ['b', '2'], ['c', '4'], ['a', '1'] ]); headers.append('b', '3'); expect(headers.entries()).to.be.iterable .and.to.deep.iterate.over([ ['a', '1'], ['b', '2, 3'], ['c', '4'] ]); }); it('should allow iterating through all headers with keys()', () => { const headers = new Headers([ ['b', '2'], ['c', '4'], ['a', '1'] ]); headers.append('b', '3'); expect(headers.keys()).to.be.iterable .and.to.iterate.over(['a', 'b', 'c']); }); it('should allow iterating through all headers with values()', () => { const headers = new Headers([ ['b', '2'], ['c', '4'], ['a', '1'] ]); headers.append('b', '3'); expect(headers.values()).to.be.iterable .and.to.iterate.over(['1', '2, 3', '4']); }); it('should reject illegal header', () => { const headers = new Headers(); expect(() => new Headers({'He y': 'ok'})).to.throw(TypeError); expect(() => new Headers({'Hé-y': 'ok'})).to.throw(TypeError); expect(() => new Headers({'He-y': 'ăk'})).to.throw(TypeError); expect(() => headers.append('Hé-y', 'ok')).to.throw(TypeError); expect(() => headers.delete('Hé-y')).to.throw(TypeError); expect(() => headers.get('Hé-y')).to.throw(TypeError); expect(() => headers.has('Hé-y')).to.throw(TypeError); expect(() => headers.set('Hé-y', 'ok')).to.throw(TypeError); // Should reject empty header expect(() => headers.append('', 'ok')).to.throw(TypeError); }); it('should ignore unsupported attributes while reading headers', () => { const FakeHeader = function () {}; // Prototypes are currently ignored // This might change in the future: #181 FakeHeader.prototype.z = 'fake'; const res = new FakeHeader(); res.a = 'string'; res.b = ['1', '2']; res.c = ''; res.d = []; res.e = 1; res.f = [1, 2]; res.g = {a: 1}; res.h = undefined; res.i = null; res.j = Number.NaN; res.k = true; res.l = false; const h1 = new Headers(res); h1.set('n', [1, 2]); h1.append('n', ['3', 4]); const h1Raw = h1.raw(); expect(h1Raw.a).to.include('string'); expect(h1Raw.b).to.include('1,2'); expect(h1Raw.c).to.include(''); expect(h1Raw.d).to.include(''); expect(h1Raw.e).to.include('1'); expect(h1Raw.f).to.include('1,2'); expect(h1Raw.g).to.include('[object Object]'); expect(h1Raw.h).to.include('undefined'); expect(h1Raw.i).to.include('null'); expect(h1Raw.j).to.include('NaN'); expect(h1Raw.k).to.include('true'); expect(h1Raw.l).to.include('false'); expect(h1Raw.n).to.include('1,2'); expect(h1Raw.n).to.include('3,4'); expect(h1Raw.z).to.be.undefined; }); it('should wrap headers', () => { const h1 = new Headers({ a: '1' }); const h1Raw = h1.raw(); const h2 = new Headers(h1); h2.set('b', '1'); const h2Raw = h2.raw(); const h3 = new Headers(h2); h3.append('a', '2'); const h3Raw = h3.raw(); expect(h1Raw.a).to.include('1'); expect(h1Raw.a).to.not.include('2'); expect(h2Raw.a).to.include('1'); expect(h2Raw.a).to.not.include('2'); expect(h2Raw.b).to.include('1'); expect(h3Raw.a).to.include('1'); expect(h3Raw.a).to.include('2'); expect(h3Raw.b).to.include('1'); }); it('should accept headers as an iterable of tuples', () => { let headers; headers = new Headers([ ['a', '1'], ['b', '2'], ['a', '3'] ]); expect(headers.get('a')).to.equal('1, 3'); expect(headers.get('b')).to.equal('2'); headers = new Headers([ new Set(['a', '1']), ['b', '2'], new Map([['a', null], ['3', null]]).keys() ]); expect(headers.get('a')).to.equal('1, 3'); expect(headers.get('b')).to.equal('2'); headers = new Headers(new Map([ ['a', '1'], ['b', '2'] ])); expect(headers.get('a')).to.equal('1'); expect(headers.get('b')).to.equal('2'); }); it('should throw a TypeError if non-tuple exists in a headers initializer', () => { expect(() => new Headers([['b', '2', 'huh?']])).to.throw(TypeError); expect(() => new Headers(['b2'])).to.throw(TypeError); expect(() => new Headers('b2')).to.throw(TypeError); expect(() => new Headers({[Symbol.iterator]: 42})).to.throw(TypeError); }); it('should use a custom inspect function', () => { const headers = new Headers([ ['Host', 'thehost'], ['Host', 'notthehost'], ['a', '1'], ['b', '2'], ['a', '3'] ]); // eslint-disable-next-line quotes expect(format(headers)).to.equal("{ a: [ '1', '3' ], b: '2', host: 'thehost' }"); }); }); node-fetch-3.3.2/test/main.js000066400000000000000000002213101445773333000160040ustar00rootroot00000000000000// Test tools import {lookup} from 'node:dns'; import crypto from 'node:crypto'; import fs from 'node:fs'; import http from 'node:http'; import path from 'node:path'; import stream from 'node:stream'; import vm from 'node:vm'; import zlib from 'node:zlib'; import {text} from 'stream-consumers'; import AbortControllerMysticatea from 'abort-controller'; import abortControllerPolyfill from 'abortcontroller-polyfill/dist/abortcontroller.js'; import chai from 'chai'; import chaiIterator from 'chai-iterator'; import chaiPromised from 'chai-as-promised'; import chaiString from 'chai-string'; import FormData from 'form-data'; import fetch, { Blob, FetchError, fileFromSync, FormData as FormDataNode, Headers, Request, Response } from '../src/index.js'; import {FetchError as FetchErrorOrig} from '../src/errors/fetch-error.js'; import HeadersOrig, {fromRawHeaders} from '../src/headers.js'; import RequestOrig from '../src/request.js'; import ResponseOrig from '../src/response.js'; import Body, {getTotalBytes, extractContentType} from '../src/body.js'; import TestServer from './utils/server.js'; import chaiTimeout from './utils/chai-timeout.js'; import {isDomainOrSubdomain, isSameProtocol} from '../src/utils/is.js'; const AbortControllerPolyfill = abortControllerPolyfill.AbortController; const encoder = new TextEncoder(); function isNodeLowerThan(version) { return !~process.version.localeCompare(version, undefined, {numeric: true}); } const { Uint8Array: VMUint8Array } = vm.runInNewContext('this'); chai.use(chaiPromised); chai.use(chaiIterator); chai.use(chaiString); chai.use(chaiTimeout); const {expect} = chai; describe('node-fetch', () => { const local = new TestServer(); let base; before(async () => { await local.start(); base = `http://${local.hostname}:${local.port}/`; }); after(async () => { return local.stop(); }); it('should return a promise', () => { const url = `${base}hello`; const p = fetch(url); expect(p).to.be.an.instanceof(Promise); expect(p).to.have.property('then'); }); it('should expose Headers, Response and Request constructors', () => { expect(FetchError).to.equal(FetchErrorOrig); expect(Headers).to.equal(HeadersOrig); expect(Response).to.equal(ResponseOrig); expect(Request).to.equal(RequestOrig); }); it('should support proper toString output for Headers, Response and Request objects', () => { expect(new Headers().toString()).to.equal('[object Headers]'); expect(new Response().toString()).to.equal('[object Response]'); expect(new Request(base).toString()).to.equal('[object Request]'); }); it('should reject with error if url is protocol relative', () => { const url = '//example.com/'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, /Invalid URL/); }); it('should reject with error if url is relative path', () => { const url = '/some/path'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, /Invalid URL/); }); it('should reject with error if protocol is unsupported', () => { const url = 'ftp://example.com/'; return expect(fetch(url)).to.eventually.be.rejectedWith(TypeError, /URL scheme "ftp" is not supported/); }); it('should reject with error on network failure', function () { this.timeout(5000); const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.include({type: 'system', code: 'ECONNREFUSED', errno: 'ECONNREFUSED'}); }); it('error should contain system error if one occurred', () => { const err = new FetchError('a message', 'system', new Error('an error')); return expect(err).to.have.property('erroredSysCall'); }); it('error should not contain system error if none occurred', () => { const err = new FetchError('a message', 'a type'); return expect(err).to.not.have.property('erroredSysCall'); }); it('system error is extracted from failed requests', function () { this.timeout(5000); const url = 'http://localhost:50000/'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('erroredSysCall'); }); it('should resolve into response', async () => { const url = `${base}hello`; const res = await fetch(url); expect(res).to.be.an.instanceof(Response); expect(res.headers).to.be.an.instanceof(Headers); expect(res.body).to.be.an.instanceof(stream.Transform); expect(res.bodyUsed).to.be.false; expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); }); it('Response.redirect should resolve into response', () => { const res = Response.redirect('http://localhost'); expect(res).to.be.an.instanceof(Response); expect(res.headers).to.be.an.instanceof(Headers); expect(res.headers.get('location')).to.equal('http://localhost/'); expect(res.status).to.equal(302); }); it('Response.redirect /w invalid url should fail', () => { expect(() => { Response.redirect('localhost'); }).to.throw(); }); it('Response.redirect /w invalid status should fail', () => { expect(() => { Response.redirect('http://localhost', 200); }).to.throw(); }); it('should accept plain text response', async () => { const url = `${base}plain`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); const result = await res.text(); expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal('text'); }); it('should accept html response (like plain text)', async () => { const url = `${base}html`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/html'); const result = await res.text(); expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal(''); }); it('should accept json response', async () => { const url = `${base}json`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('application/json'); const result = await res.json(); expect(res.bodyUsed).to.be.true; expect(result).to.be.an('object'); expect(result).to.deep.equal({name: 'value'}); }); it('should send request with custom headers', async () => { const url = `${base}inspect`; const options = { headers: {'x-custom-header': 'abc'} }; const res = await fetch(url, options); const result = await res.json(); expect(result.headers['x-custom-header']).to.equal('abc'); }); it('should accept headers instance', async () => { const url = `${base}inspect`; const options = { headers: new Headers({'x-custom-header': 'abc'}) }; const res = await fetch(url, options); const json = await res.json(); expect(json.headers['x-custom-header']).to.equal('abc'); }); it('should accept custom host header', async () => { const url = `${base}inspect`; const options = { headers: { host: 'example.com' } }; const res = await fetch(url, options); const json = await res.json(); expect(json.headers.host).to.equal('example.com'); }); it('should accept custom HoSt header', async () => { const url = `${base}inspect`; const options = { headers: { HoSt: 'example.com' } }; const res = await fetch(url, options); const json = await res.json(); expect(json.headers.host).to.equal('example.com'); }); it('should follow redirect code 301', async () => { const url = `${base}redirect/301`; const res = await fetch(url); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); expect(res.ok).to.be.true; await res.arrayBuffer(); }); it('should follow redirect code 302', async () => { const url = `${base}redirect/302`; const res = await fetch(url); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); await res.arrayBuffer(); }); it('should follow redirect code 303', async () => { const url = `${base}redirect/303`; const res = await fetch(url); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); await res.arrayBuffer(); }); it('should follow redirect code 307', async () => { const url = `${base}redirect/307`; const res = await fetch(url); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); await res.arrayBuffer(); }); it('should follow redirect code 308', async () => { const url = `${base}redirect/308`; const res = await fetch(url); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); await res.arrayBuffer(); }); it('should follow redirect chain', async () => { const url = `${base}redirect/chain`; const res = await fetch(url); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); await res.arrayBuffer(); }); it('should follow POST request redirect code 301 with GET', async () => { const url = `${base}redirect/301`; const options = { method: 'POST', body: 'a=1' }; const res = await fetch(url, options); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); const result = await res.json(); expect(result.method).to.equal('GET'); expect(result.body).to.equal(''); }); it('should follow PATCH request redirect code 301 with PATCH', async () => { const url = `${base}redirect/301`; const options = { method: 'PATCH', body: 'a=1' }; const res = await fetch(url, options); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); const json = await res.json(); expect(json.method).to.equal('PATCH'); expect(json.body).to.equal('a=1'); }); it('should follow POST request redirect code 302 with POST', async () => { const url = `${base}redirect/302`; const options = { method: 'POST', body: 'a=1' }; const res = await fetch(url, options); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); const result = await res.json(); expect(result.method).to.equal('GET'); expect(result.body).to.equal(''); }); it('should follow PATCH request redirect code 302 with PATCH', async () => { const url = `${base}redirect/302`; const options = { method: 'PATCH', body: 'a=1' }; const res = await fetch(url, options); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); const json = await res.json(); expect(json.method).to.equal('PATCH'); expect(json.body).to.equal('a=1'); }); it('should follow redirect code 303 with GET', async () => { const url = `${base}redirect/303`; const options = { method: 'PUT', body: 'a=1' }; const res = await fetch(url, options); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); const result = await res.json(); expect(result.method).to.equal('GET'); expect(result.body).to.equal(''); }); it('should follow PATCH request redirect code 307 with PATCH', async () => { const url = `${base}redirect/307`; const options = { method: 'PATCH', body: 'a=1' }; const res = await fetch(url, options); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); const result = await res.json(); expect(result.method).to.equal('PATCH'); expect(result.body).to.equal('a=1'); }); it('should not follow non-GET redirect if body is a readable stream', () => { const url = `${base}redirect/307`; const options = { method: 'PATCH', body: stream.Readable.from('tada') }; return expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'unsupported-redirect'); }); it('should obey maximum redirect, reject case', () => { const url = `${base}redirect/chain`; const options = { follow: 1 }; return expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-redirect'); }); it('should obey redirect chain, resolve case', async () => { const url = `${base}redirect/chain`; const options = { follow: 2 }; const res = await fetch(url, options); expect(res.url).to.equal(`${base}inspect`); expect(res.status).to.equal(200); await res.arrayBuffer(); }); it('should allow not following redirect', () => { const url = `${base}redirect/301`; const options = { follow: 0 }; return expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-redirect'); }); it('should support redirect mode, manual flag', async () => { const url = `${base}redirect/301`; const options = { redirect: 'manual' }; const res = await fetch(url, options); expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.equal('/inspect'); const locationURL = new URL(res.headers.get('location'), url); expect(locationURL.href).to.equal(`${base}inspect`); }); it('should support redirect mode, manual flag, broken Location header', async () => { const url = `${base}redirect/bad-location`; const options = { redirect: 'manual' }; const res = await fetch(url, options); expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.equal('<>'); const locationURL = new URL(res.headers.get('location'), url); expect(locationURL.href).to.equal(`${base}redirect/%3C%3E`); await res.arrayBuffer(); }); it('should support redirect mode to other host, manual flag', async () => { const url = `${base}redirect/301/otherhost`; const options = { redirect: 'manual' }; const res = await fetch(url, options); expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.equal('https://github.com/node-fetch'); }); it('should support redirect mode, error flag', () => { const url = `${base}redirect/301`; const options = { redirect: 'error' }; return expect(fetch(url, options)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'no-redirect'); }); it('should support redirect mode, manual flag when there is no redirect', async () => { const url = `${base}hello`; const options = { redirect: 'manual' }; const res = await fetch(url, options); expect(res.url).to.equal(url); expect(res.status).to.equal(200); expect(res.headers.get('location')).to.be.null; await res.arrayBuffer(); }); it('should follow redirect code 301 and keep existing headers', async () => { const url = `${base}redirect/301`; const options = { headers: new Headers({'x-custom-header': 'abc'}) }; const res = await fetch(url, options); expect(res.url).to.equal(`${base}inspect`); const json = await res.json(); expect(json.headers['x-custom-header']).to.equal('abc'); }); it('should not forward secure headers to 3th party', async () => { const res = await fetch(`${base}redirect-to/302/https://httpbin.org/get`, { headers: new Headers({ cookie: 'gets=removed', cookie2: 'gets=removed', authorization: 'gets=removed', 'www-authenticate': 'gets=removed', 'other-safe-headers': 'stays', 'x-foo': 'bar' }) }); const headers = new Headers((await res.json()).headers); // Safe headers are not removed expect(headers.get('other-safe-headers')).to.equal('stays'); expect(headers.get('x-foo')).to.equal('bar'); // Unsafe headers should not have been sent to httpbin expect(headers.get('cookie')).to.equal(null); expect(headers.get('cookie2')).to.equal(null); expect(headers.get('www-authenticate')).to.equal(null); expect(headers.get('authorization')).to.equal(null); }); it('should forward secure headers to same host', async () => { const res = await fetch(`${base}redirect-to/302/${base}inspect`, { headers: new Headers({ cookie: 'is=cookie', cookie2: 'is=cookie2', authorization: 'is=authorization', 'other-safe-headers': 'stays', 'www-authenticate': 'is=www-authenticate', 'x-foo': 'bar' }) }); const headers = new Headers((await res.json()).headers); // Safe headers are not removed expect(res.url).to.equal(`${base}inspect`); expect(headers.get('other-safe-headers')).to.equal('stays'); expect(headers.get('x-foo')).to.equal('bar'); // Unsafe headers are not removed expect(headers.get('cookie')).to.equal('is=cookie'); expect(headers.get('cookie2')).to.equal('is=cookie2'); expect(headers.get('www-authenticate')).to.equal('is=www-authenticate'); expect(headers.get('authorization')).to.equal('is=authorization'); }); it('isDomainOrSubdomain', () => { // Forwarding headers to same (sub)domain are OK expect(isDomainOrSubdomain('http://a.com', 'http://a.com')).to.be.true; expect(isDomainOrSubdomain('http://a.com', 'http://www.a.com')).to.be.true; expect(isDomainOrSubdomain('http://a.com', 'http://foo.bar.a.com')).to.be.true; // Forwarding headers to parent domain, another sibling or a totally other domain is not ok expect(isDomainOrSubdomain('http://b.com', 'http://a.com')).to.be.false; expect(isDomainOrSubdomain('http://www.a.com', 'http://a.com')).to.be.false; expect(isDomainOrSubdomain('http://bob.uk.com', 'http://uk.com')).to.be.false; expect(isDomainOrSubdomain('http://bob.uk.com', 'http://xyz.uk.com')).to.be.false; }); it('should not forward secure headers to changed protocol', async () => { const res = await fetch('https://httpbin.org/redirect-to?url=http%3A%2F%2Fhttpbin.org%2Fget&status_code=302', { headers: new Headers({ cookie: 'gets=removed', cookie2: 'gets=removed', authorization: 'gets=removed', 'www-authenticate': 'gets=removed', 'other-safe-headers': 'stays', 'x-foo': 'bar' }) }); const headers = new Headers((await res.json()).headers); // Safe headers are not removed expect(headers.get('other-safe-headers')).to.equal('stays'); expect(headers.get('x-foo')).to.equal('bar'); // Unsafe headers should not have been sent to downgraded http expect(headers.get('cookie')).to.equal(null); expect(headers.get('cookie2')).to.equal(null); expect(headers.get('www-authenticate')).to.equal(null); expect(headers.get('authorization')).to.equal(null); }); it('isSameProtocol', () => { // Forwarding headers to same protocol is OK expect(isSameProtocol('http://a.com', 'http://a.com')).to.be.true; expect(isSameProtocol('https://a.com', 'https://www.a.com')).to.be.true; // Forwarding headers to diff protocol is not OK expect(isSameProtocol('http://b.com', 'https://b.com')).to.be.false; expect(isSameProtocol('http://www.a.com', 'https://a.com')).to.be.false; }); it('should treat broken redirect as ordinary response (follow)', async () => { const url = `${base}redirect/no-location`; const res = await fetch(url); expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.be.null; await res.arrayBuffer(); }); it('should treat broken redirect as ordinary response (manual)', async () => { const url = `${base}redirect/no-location`; const options = { redirect: 'manual' }; const res = await fetch(url, options); expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.be.null; }); it('should process an invalid redirect (manual)', async () => { const url = `${base}redirect/301/invalid`; const options = { redirect: 'manual' }; const res = await fetch(url, options); expect(res.url).to.equal(url); expect(res.status).to.equal(301); expect(res.headers.get('location')).to.equal('//super:invalid:url%/'); await res.arrayBuffer(); }); it('should throw an error on invalid redirect url', async () => { const url = `${base}redirect/301/invalid`; return fetch(url).then(() => { expect.fail(); }, error => { expect(error).to.be.an.instanceof(FetchError); expect(error.message).to.equal('uri requested responds with an invalid redirect URL: //super:invalid:url%/'); }); }); it('should throw a TypeError on an invalid redirect option', () => { const url = `${base}redirect/301`; const options = { redirect: 'foobar' }; return fetch(url, options).then(() => { expect.fail(); }, error => { expect(error).to.be.an.instanceOf(TypeError); expect(error.message).to.equal('Redirect option \'foobar\' is not a valid value of RequestRedirect'); }); }); it('should set redirected property on response when redirect', async () => { const url = `${base}redirect/301`; const res = await fetch(url); expect(res.redirected).to.be.true; await res.arrayBuffer(); }); it('should not set redirected property on response without redirect', async () => { const url = `${base}hello`; const res = await fetch(url); expect(res.redirected).to.be.false; }); it('should ignore invalid headers', () => { const headers = fromRawHeaders([ 'Invalid-Header ', 'abc\r\n', 'Invalid-Header-Value', '\u0007k\r\n', 'Cookie', '\u0007k\r\n', 'Cookie', '\u0007kk\r\n' ]); expect(headers).to.be.instanceOf(Headers); expect(headers.raw()).to.deep.equal({}); }); it('should handle client-error response', async () => { const url = `${base}error/400`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.status).to.equal(400); expect(res.statusText).to.equal('Bad Request'); expect(res.ok).to.be.false; const result = await res.text(); expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal('client error'); }); it('should handle server-error response', async () => { const url = `${base}error/500`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.status).to.equal(500); expect(res.statusText).to.equal('Internal Server Error'); expect(res.ok).to.be.false; const result = await res.text(); expect(res.bodyUsed).to.be.true; expect(result).to.be.a('string'); expect(result).to.equal('server error'); }); it('should handle network-error response', () => { const url = `${base}error/reset`; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'ECONNRESET'); }); it('should handle network-error partial response', async () => { const url = `${base}error/premature`; const res = await fetch(url); expect(res.status).to.equal(200); expect(res.ok).to.be.true; await expect(res.text()).to.eventually.be.rejectedWith(Error) .and.have.property('message').matches(/Premature close|The operation was aborted|aborted/); }); it('should handle network-error in chunked response', async () => { const url = `${base}error/premature/chunked`; const res = await fetch(url); expect(res.status).to.equal(200); expect(res.ok).to.be.true; return expect(new Promise((resolve, reject) => { res.body.on('error', reject); res.body.on('close', resolve); })).to.eventually.be.rejectedWith(Error, 'Premature close') .and.have.property('code', 'ERR_STREAM_PREMATURE_CLOSE'); }); it('should handle network-error in chunked response async iterator', async () => { const url = `${base}error/premature/chunked`; const res = await fetch(url); expect(res.status).to.equal(200); expect(res.ok).to.be.true; const read = async body => { const chunks = []; if (isNodeLowerThan('v14.15.2')) { // In older Node.js versions, some errors don't come out in the async iterator; we have // to pick them up from the event-emitter and then throw them after the async iterator let error; body.on('error', err => { error = err; }); for await (const chunk of body) { chunks.push(chunk); } if (error) { throw error; } return new Promise(resolve => { body.on('close', () => resolve(chunks)); }); } for await (const chunk of body) { chunks.push(chunk); } return chunks; }; return expect(read(res.body)) .to.eventually.be.rejectedWith(Error, 'Premature close') .and.have.property('code', 'ERR_STREAM_PREMATURE_CLOSE'); }); it('should handle network-error in chunked response in consumeBody', async () => { const url = `${base}error/premature/chunked`; const res = await fetch(url); expect(res.status).to.equal(200); expect(res.ok).to.be.true; return expect(res.text()) .to.eventually.be.rejectedWith(Error, 'Premature close'); }); it('should follow redirect after empty chunked transfer-encoding', async () => { const url = `${base}redirect/chunked`; const res = await fetch(url); expect(res.status).to.equal(200); expect(res.ok).to.be.true; }); it('should handle chunked response with more than 1 chunk in the final packet', async () => { const url = `${base}chunked/multiple-ending`; const res = await fetch(url); expect(res.ok).to.be.true; const result = await res.text(); expect(result).to.equal('foobar'); }); it('should handle chunked response with final chunk and EOM in separate packets', async () => { const url = `${base}chunked/split-ending`; const res = await fetch(url); expect(res.ok).to.be.true; const result = await res.text(); expect(result).to.equal('foobar'); }); it('should handle DNS-error response', () => { const url = 'http://domain.invalid'; return expect(fetch(url)).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code').that.matches(/ENOTFOUND|EAI_AGAIN/); }); it('should reject invalid json response', async () => { const url = `${base}error/json`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('application/json'); return expect(res.json()).to.eventually.be.rejectedWith(Error); }); it('should handle response with no status text', async () => { const url = `${base}no-status-text`; const res = await fetch(url); expect(res.statusText).to.equal(''); await res.arrayBuffer(); }); it('should handle no content response', async () => { const url = `${base}no-content`; const res = await fetch(url); expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); expect(res.ok).to.be.true; const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.be.empty; }); it('should reject when trying to parse no content response as json', async () => { const url = `${base}no-content`; const res = await fetch(url); expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); expect(res.ok).to.be.true; return expect(res.json()).to.eventually.be.rejectedWith(Error); }); it('should handle no content response with gzip encoding', async () => { const url = `${base}no-content/gzip`; const res = await fetch(url); expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); expect(res.headers.get('content-encoding')).to.equal('gzip'); expect(res.ok).to.be.true; const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.be.empty; }); it('should handle not modified response', async () => { const url = `${base}not-modified`; const res = await fetch(url); expect(res.status).to.equal(304); expect(res.statusText).to.equal('Not Modified'); expect(res.ok).to.be.false; const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.be.empty; }); it('should handle not modified response with gzip encoding', async () => { const url = `${base}not-modified/gzip`; const res = await fetch(url); expect(res.status).to.equal(304); expect(res.statusText).to.equal('Not Modified'); expect(res.headers.get('content-encoding')).to.equal('gzip'); expect(res.ok).to.be.false; const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.be.empty; }); it('should decompress gzip response', async () => { const url = `${base}gzip`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); it('should decompress slightly invalid gzip response', async () => { const url = `${base}gzip-truncated`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); it('should make capitalised Content-Encoding lowercase', async () => { const url = `${base}gzip-capital`; const res = await fetch(url); expect(res.headers.get('content-encoding')).to.equal('gzip'); const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); it('should decompress deflate response', async () => { const url = `${base}deflate`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); it('should decompress deflate raw response from old apache server', async () => { const url = `${base}deflate-raw`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); it('should handle empty deflate response', async () => { const url = `${base}empty/deflate`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); const text = await res.text(); expect(text).to.be.a('string'); expect(text).to.be.empty; }); it('should decompress brotli response', async function () { if (typeof zlib.createBrotliDecompress !== 'function') { this.skip(); } const url = `${base}brotli`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.equal('hello world'); }); it('should handle no content response with brotli encoding', async function () { if (typeof zlib.createBrotliDecompress !== 'function') { this.skip(); } const url = `${base}no-content/brotli`; const res = await fetch(url); expect(res.status).to.equal(204); expect(res.statusText).to.equal('No Content'); expect(res.headers.get('content-encoding')).to.equal('br'); expect(res.ok).to.be.true; const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.be.empty; }); it('should skip decompression if unsupported', async () => { const url = `${base}sdch`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.equal('fake sdch string'); }); it('should reject if response compression is invalid', async () => { const url = `${base}invalid-content-encoding`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'Z_DATA_ERROR'); }); it('should handle errors on the body stream even if it is not used', done => { const url = `${base}invalid-content-encoding`; fetch(url) .then(res => { expect(res.status).to.equal(200); }) .catch(() => {}) .then(() => { // Wait a few ms to see if a uncaught error occurs setTimeout(() => { done(); }, 20); }); }); it('should collect handled errors on the body stream to reject if the body is used later', async () => { const url = `${base}invalid-content-encoding`; const res = await fetch(url); await new Promise(resolve => { setTimeout(() => resolve(), 20); }); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('code', 'Z_DATA_ERROR'); }); it('should allow disabling auto decompression', async () => { const url = `${base}gzip`; const options = { compress: false }; const res = await fetch(url, options); expect(res.headers.get('content-type')).to.equal('text/plain'); const result = await res.text(); expect(result).to.be.a('string'); expect(result).to.not.equal('hello world'); }); it('should not overwrite existing accept-encoding header when auto decompression is true', async () => { const url = `${base}inspect`; const options = { compress: true, headers: { 'Accept-Encoding': 'gzip' } }; const res = await fetch(url, options); const json = await res.json(); expect(json.headers['accept-encoding']).to.equal('gzip'); }); const testAbortController = (name, buildAbortController, moreTests = null) => { describe(`AbortController (${name})`, () => { let controller; beforeEach(() => { controller = buildAbortController(); }); it('should support request cancellation with signal', () => { const promise = fetch(`${base}timeout`, { method: 'POST', signal: controller.signal, headers: { 'Content-Type': 'application/json', body: '{"hello": "world"}' } }); controller.abort(); return expect(promise) .to.eventually.be.rejected .and.be.an.instanceOf(Error) .and.include({ type: 'aborted', name: 'AbortError' }); }); it('should support multiple request cancellation with signal', () => { const fetches = [ fetch(`${base}timeout`, {signal: controller.signal}), fetch(`${base}timeout`, { method: 'POST', signal: controller.signal, headers: { 'Content-Type': 'application/json', body: JSON.stringify({hello: 'world'}) } }) ]; controller.abort(); return Promise.all(fetches.map(fetched => expect(fetched) .to.eventually.be.rejected .and.be.an.instanceOf(Error) .and.include({ type: 'aborted', name: 'AbortError' }) )); }); it('should reject immediately if signal has already been aborted', () => { const url = `${base}timeout`; const options = { signal: controller.signal }; controller.abort(); const fetched = fetch(url, options); return expect(fetched).to.eventually.be.rejected .and.be.an.instanceOf(Error) .and.include({ type: 'aborted', name: 'AbortError' }); }); it('should allow redirects to be aborted', () => { const request = new Request(`${base}redirect/slow`, { signal: controller.signal }); setTimeout(() => { controller.abort(); }, 20); return expect(fetch(request)).to.be.eventually.rejected .and.be.an.instanceOf(Error) .and.have.property('name', 'AbortError'); }); it('should allow redirected response body to be aborted', async () => { const request = new Request(`${base}redirect/slow-stream`, { signal: controller.signal }); const res = await fetch(request); expect(res.headers.get('content-type')).to.equal('text/plain'); controller.abort(); return expect(res.text()).to.be.eventually.rejected .and.be.an.instanceOf(Error) .and.have.property('name', 'AbortError'); }); it('should reject response body with AbortError when aborted before stream has been read completely', () => { return expect(fetch( `${base}slow`, {signal: controller.signal} )) .to.eventually.be.fulfilled .then(res => { const promise = res.text(); controller.abort(); return expect(promise) .to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError'); }); }); it('should reject response body methods immediately with AbortError when aborted before stream is disturbed', () => { return expect(fetch( `${base}slow`, {signal: controller.signal} )) .to.eventually.be.fulfilled .then(res => { controller.abort(); return expect(res.text()) .to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError'); }); }); it('should emit error event to response body with an AbortError when aborted before underlying stream is closed', done => { expect(fetch( `${base}slow`, {signal: controller.signal} )) .to.eventually.be.fulfilled .then(res => { res.body.once('error', err => { expect(err) .to.be.an.instanceof(Error) .and.have.property('name', 'AbortError'); done(); }); controller.abort(); }); }); it('should cancel request body of type Stream with AbortError when aborted', () => { const body = new stream.Readable({objectMode: true}); body._read = () => {}; const promise = fetch(`${base}slow`, { method: 'POST', signal: controller.signal, body }); const result = Promise.all([ new Promise((resolve, reject) => { body.on('error', error => { try { expect(error).to.be.an.instanceof(Error).and.have.property('name', 'AbortError'); resolve(); } catch (error_) { reject(error_); } }); }), expect(promise).to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError') ]); controller.abort(); return result; }); if (moreTests) { moreTests(); } }); }; testAbortController('polyfill', () => new AbortControllerPolyfill(), () => { it('should remove internal AbortSignal event listener after request is aborted', () => { const controller = new AbortControllerPolyfill(); const {signal} = controller; setTimeout(() => { controller.abort(); }, 20); return expect(fetch(`${base}timeout`, {signal})) .to.eventually.be.rejected .and.be.an.instanceof(Error) .and.have.property('name', 'AbortError') .then(() => { return expect(signal.listeners.abort.length).to.equal(0); }); }); it('should remove internal AbortSignal event listener after request and response complete without aborting', () => { const controller = new AbortControllerPolyfill(); const {signal} = controller; const fetchHtml = fetch(`${base}html`, {signal}) .then(res => res.text()); const fetchResponseError = fetch(`${base}error/reset`, {signal}); const fetchRedirect = fetch(`${base}redirect/301`, {signal}).then(res => res.json()); return Promise.all([ expect(fetchHtml).to.eventually.be.fulfilled.and.equal(''), expect(fetchResponseError).to.be.eventually.rejected, expect(fetchRedirect).to.eventually.be.fulfilled ]).then(() => { expect(signal.listeners.abort.length).to.equal(0); }); }); } ); testAbortController('mysticatea', () => new AbortControllerMysticatea()); if (process.version > 'v15') { testAbortController('native', () => new AbortController()); } it('should throw a TypeError if a signal is not of type AbortSignal or EventTarget', () => { return Promise.all([ expect(fetch(`${base}inspect`, {signal: {}})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), expect(fetch(`${base}inspect`, {signal: ''})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal'), expect(fetch(`${base}inspect`, {signal: Object.create(null)})) .to.be.eventually.rejected .and.be.an.instanceof(TypeError) .and.have.property('message').includes('AbortSignal') ]); }); it('should gracefully handle a nullish signal', () => { return Promise.all([ fetch(`${base}hello`, {signal: null}).then(res => { return expect(res.ok).to.be.true; }), fetch(`${base}hello`, {signal: undefined}).then(res => { return expect(res.ok).to.be.true; }) ]); }); it('should set default User-Agent', () => { const url = `${base}inspect`; return fetch(url).then(res => res.json()).then(res => { expect(res.headers['user-agent']).to.startWith('node-fetch'); }); }); it('should allow setting User-Agent', () => { const url = `${base}inspect`; const options = { headers: { 'user-agent': 'faked' } }; return fetch(url, options).then(res => res.json()).then(res => { expect(res.headers['user-agent']).to.equal('faked'); }); }); it('should set default Accept header', async () => { const url = `${base}inspect`; const res = await fetch(url); const json = await res.json(); expect(json.headers.accept).to.equal('*/*'); }); it('should allow setting Accept header', async () => { const url = `${base}inspect`; const options = { headers: { accept: 'application/json' } }; const res = await fetch(url, options); const json = await res.json(); expect(json.headers.accept).to.equal('application/json'); }); it('should allow POST request', async () => { const url = `${base}inspect`; const options = { method: 'POST' }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.be.undefined; expect(json.headers['content-length']).to.equal('0'); }); it('should allow POST request with string body', async () => { const url = `${base}inspect`; const options = { method: 'POST', body: 'a=1' }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('a=1'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.equal('text/plain;charset=UTF-8'); expect(json.headers['content-length']).to.equal('3'); }); it('should allow POST request with ArrayBuffer body', async () => { const url = `${base}inspect`; const options = { method: 'POST', body: encoder.encode('Hello, world!\n').buffer }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('Hello, world!\n'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.be.undefined; expect(json.headers['content-length']).to.equal('14'); }); it('should allow POST request with ArrayBuffer body from a VM context', async () => { const url = `${base}inspect`; const options = { method: 'POST', body: new VMUint8Array(encoder.encode('Hello, world!\n')).buffer }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('Hello, world!\n'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.be.undefined; expect(json.headers['content-length']).to.equal('14'); }); it('should allow POST request with ArrayBufferView (Uint8Array) body', async () => { const url = `${base}inspect`; const options = { method: 'POST', body: encoder.encode('Hello, world!\n') }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('Hello, world!\n'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.be.undefined; expect(json.headers['content-length']).to.equal('14'); }); it('should allow POST request with ArrayBufferView (DataView) body', async () => { const url = `${base}inspect`; const options = { method: 'POST', body: new DataView(encoder.encode('Hello, world!\n').buffer) }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('Hello, world!\n'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.be.undefined; expect(json.headers['content-length']).to.equal('14'); }); it('should allow POST request with ArrayBufferView (Uint8Array) body from a VM context', async () => { const url = `${base}inspect`; const options = { method: 'POST', body: new VMUint8Array(encoder.encode('Hello, world!\n')) }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('Hello, world!\n'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.be.undefined; expect(json.headers['content-length']).to.equal('14'); }); it('should allow POST request with ArrayBufferView (Uint8Array, offset, length) body', async () => { const url = `${base}inspect`; const options = { method: 'POST', body: encoder.encode('Hello, world!\n').subarray(7, 13) }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('world!'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.be.undefined; expect(json.headers['content-length']).to.equal('6'); }); it('should allow POST request with blob body without type', async () => { const url = `${base}inspect`; const options = { method: 'POST', body: new Blob(['a=1']) }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('a=1'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.be.undefined; expect(json.headers['content-length']).to.equal('3'); }); it('should allow POST request with blob body with type', async () => { const url = `${base}inspect`; const options = { method: 'POST', body: new Blob(['a=1'], { type: 'text/plain;charset=UTF-8' }) }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('a=1'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.equal('text/plain;charset=UTF-8'); expect(json.headers['content-length']).to.equal('3'); }); it('should allow POST request with readable stream as body', async () => { const url = `${base}inspect`; const options = { method: 'POST', body: stream.Readable.from('a=1') }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('a=1'); expect(json.headers['transfer-encoding']).to.equal('chunked'); expect(json.headers['content-type']).to.be.undefined; expect(json.headers['content-length']).to.be.undefined; }); it('should reject if the request body stream emits an error', () => { const url = `${base}inspect`; const requestBody = new stream.PassThrough(); const options = { method: 'POST', body: requestBody }; const errorMessage = 'request body stream error'; setImmediate(() => { requestBody.emit('error', new Error(errorMessage)); }); return expect(fetch(url, options)) .to.be.rejectedWith(Error, errorMessage); }); it('should allow POST request with form-data as body', async () => { const form = new FormData(); form.append('a', '1'); const url = `${base}multipart`; const options = { method: 'POST', body: form }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.headers['content-type']).to.startWith('multipart/form-data;boundary='); expect(json.headers['content-length']).to.be.a('string'); expect(json.body).to.equal('a=1'); }); it('should allow POST request with form-data using stream as body', async () => { const form = new FormData(); form.append('my_field', fs.createReadStream('test/utils/dummy.txt')); const url = `${base}multipart`; const options = { method: 'POST', body: form }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.headers['content-type']).to.startWith('multipart/form-data;boundary='); expect(json.headers['content-length']).to.be.undefined; expect(json.body).to.contain('my_field='); }); it('should allow POST request with form-data as body and custom headers', async () => { const form = new FormData(); form.append('a', '1'); const headers = form.getHeaders(); headers.b = '2'; const url = `${base}multipart`; const options = { method: 'POST', body: form, headers }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.headers['content-type']).to.startWith('multipart/form-data; boundary='); expect(json.headers['content-length']).to.be.a('string'); expect(json.headers.b).to.equal('2'); expect(json.body).to.equal('a=1'); }); it('should support spec-compliant form-data as POST body', async () => { const form = new FormDataNode(); const filename = path.join('test', 'utils', 'dummy.txt'); form.set('field', 'some text'); form.set('file', fileFromSync(filename)); const url = `${base}multipart`; const options = { method: 'POST', body: form }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.headers['content-type']).to.startWith('multipart/form-data'); expect(json.body).to.contain('field='); expect(json.body).to.contain('file='); }); it('should allow POST request with object body', async () => { const url = `${base}inspect`; // Note that fetch simply calls tostring on an object const options = { method: 'POST', body: {a: 1} }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('[object Object]'); expect(json.headers['content-type']).to.equal('text/plain;charset=UTF-8'); expect(json.headers['content-length']).to.equal('15'); }); it('constructing a Response with URLSearchParams as body should have a Content-Type', async () => { const parameters = new URLSearchParams(); const res = new Response(parameters); res.headers.get('Content-Type'); expect(res.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); it('constructing a Request with URLSearchParams as body should have a Content-Type', async () => { const parameters = new URLSearchParams(); const request = new Request(base, {method: 'POST', body: parameters}); expect(request.headers.get('Content-Type')).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); }); it('Reading a body with URLSearchParams should echo back the result', async () => { const parameters = new URLSearchParams(); parameters.append('a', '1'); const text = await new Response(parameters).text(); expect(text).to.equal('a=1'); }); // Body should been cloned... it('constructing a Request/Response with URLSearchParams and mutating it should not affected body', async () => { const parameters = new URLSearchParams(); const request = new Request(`${base}inspect`, {method: 'POST', body: parameters}); parameters.append('a', '1'); const text = await request.text(); expect(text).to.equal(''); }); it('should allow POST request with URLSearchParams as body', async () => { const parameters = new URLSearchParams(); parameters.append('a', '1'); const url = `${base}inspect`; const options = { method: 'POST', body: parameters }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); expect(json.headers['content-length']).to.equal('3'); expect(json.body).to.equal('a=1'); }); it('should still recognize URLSearchParams when extended', async () => { class CustomSearchParameters extends URLSearchParams {} const parameters = new CustomSearchParameters(); parameters.append('a', '1'); const url = `${base}inspect`; const options = { method: 'POST', body: parameters }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); expect(json.headers['content-length']).to.equal('3'); expect(json.body).to.equal('a=1'); }); /* For 100% code coverage, checks for duck-typing-only detection * where both constructor.name and brand tests fail */ it('should still recognize URLSearchParams when extended from polyfill', async () => { class CustomPolyfilledSearchParameters extends URLSearchParams {} const parameters = new CustomPolyfilledSearchParameters(); parameters.append('a', '1'); const url = `${base}inspect`; const options = { method: 'POST', body: parameters }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.headers['content-type']).to.equal('application/x-www-form-urlencoded;charset=UTF-8'); expect(json.headers['content-length']).to.equal('3'); expect(json.body).to.equal('a=1'); }); it('should overwrite Content-Length if possible', async () => { const url = `${base}inspect`; // Note that fetch simply calls tostring on an object const options = { method: 'POST', headers: { 'Content-Length': '1000' }, body: 'a=1' }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('POST'); expect(json.body).to.equal('a=1'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-type']).to.equal('text/plain;charset=UTF-8'); expect(json.headers['content-length']).to.equal('3'); }); it('should allow PUT request', async () => { const url = `${base}inspect`; const options = { method: 'PUT', body: 'a=1' }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('PUT'); expect(json.body).to.equal('a=1'); }); it('should allow DELETE request', async () => { const url = `${base}inspect`; const options = { method: 'DELETE' }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('DELETE'); }); it('should allow DELETE request with string body', async () => { const url = `${base}inspect`; const options = { method: 'DELETE', body: 'a=1' }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('DELETE'); expect(json.body).to.equal('a=1'); expect(json.headers['transfer-encoding']).to.be.undefined; expect(json.headers['content-length']).to.equal('3'); }); it('should allow PATCH request', async () => { const url = `${base}inspect`; const options = { method: 'PATCH', body: 'a=1' }; const res = await fetch(url, options); const json = await res.json(); expect(json.method).to.equal('PATCH'); expect(json.body).to.equal('a=1'); }); it('should allow HEAD request', async () => { const url = `${base}hello`; const options = { method: 'HEAD' }; const res = await fetch(url, options); const text = await res.text(); expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); expect(res.headers.get('content-type')).to.equal('text/plain'); expect(res.body).to.be.an.instanceof(stream.Transform); expect(text).to.equal(''); }); it('should allow HEAD request with content-encoding header', async () => { const url = `${base}error/404`; const options = { method: 'HEAD' }; const res = await fetch(url, options); const text = await res.text(); expect(res.status).to.equal(404); expect(res.headers.get('content-encoding')).to.equal('gzip'); expect(text).to.equal(''); }); it('should allow OPTIONS request', async () => { const url = `${base}options`; const options = { method: 'OPTIONS' }; const res = await fetch(url, options); expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); expect(res.headers.get('allow')).to.equal('GET, HEAD, OPTIONS'); expect(res.body).to.be.an.instanceof(stream.Transform); await res.arrayBuffer(); }); it('should reject decoding body twice', async () => { const url = `${base}plain`; const res = await fetch(url); expect(res.headers.get('content-type')).to.equal('text/plain'); await res.text(); expect(res.bodyUsed).to.be.true; return expect(res.text()).to.eventually.be.rejectedWith(Error); }); it('should support maximum response size, multiple chunk', async () => { const url = `${base}size/chunk`; const options = { size: 5 }; const res = await fetch(url, options); expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-size'); }); it('should support maximum response size, single chunk', async () => { const url = `${base}size/long`; const options = { size: 5 }; const res = await fetch(url, options); expect(res.status).to.equal(200); expect(res.headers.get('content-type')).to.equal('text/plain'); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.have.property('type', 'max-size'); }); it('should allow piping response body as stream', async () => { const url = `${base}hello`; const res = await fetch(url); expect(res.body).to.be.an.instanceof(stream.Transform); const body = await text(res.body); expect(body).to.equal('world'); }); it('should allow cloning a response, and use both as stream', async () => { const url = `${base}hello`; const res = await fetch(url); const r1 = res.clone(); expect(res.body).to.be.an.instanceof(stream.Transform); expect(r1.body).to.be.an.instanceof(stream.Transform); const [t1, t2] = await Promise.all([ text(res.body), text(r1.body) ]); expect(t1).to.equal('world'); expect(t2).to.equal('world'); }); it('should allow cloning a json response and log it as text response', async () => { const url = `${base}json`; const res = await fetch(url); const r1 = res.clone(); const results = await Promise.all([res.json(), r1.text()]); expect(results[0]).to.deep.equal({name: 'value'}); expect(results[1]).to.equal('{"name":"value"}'); }); it('should allow cloning a json response, and then log it as text response', async () => { const url = `${base}json`; const res = await fetch(url); const r1 = res.clone(); const json = await res.json(); expect(json).to.deep.equal({name: 'value'}); const text = await r1.text(); expect(text).to.equal('{"name":"value"}'); }); it('should allow cloning a json response, first log as text response, then return json object', async () => { const url = `${base}json`; const res = await fetch(url); const r1 = res.clone(); const text = await r1.text(); expect(text).to.equal('{"name":"value"}'); const json = await res.json(); expect(json).to.deep.equal({name: 'value'}); }); it('should not allow cloning a response after its been used', async () => { const url = `${base}hello`; const res = await fetch(url); await res.text(); expect(() => { res.clone(); }).to.throw(Error); }); it('the default highWaterMark should equal 16384', async () => { const url = `${base}hello`; const res = await fetch(url); expect(res.highWaterMark).to.equal(16384); }); it('should timeout on cloning response without consuming one of the streams when the second packet size is equal default highWaterMark', function () { this.timeout(300); const url = local.mockResponse(res => { // Observed behavior of TCP packets splitting: // - response body size <= 65438 → single packet sent // - response body size > 65438 → multiple packets sent // Max TCP packet size is 64kB (https://stackoverflow.com/a/2614188/5763764), // but first packet probably transfers more than the response body. const firstPacketMaxSize = 65438; const secondPacketSize = 16 * 1024; // = defaultHighWaterMark res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize)); }); return expect( fetch(url).then(res => res.clone().buffer()) ).to.timeout; }); it('should timeout on cloning response without consuming one of the streams when the second packet size is equal custom highWaterMark', function () { this.timeout(300); const url = local.mockResponse(res => { const firstPacketMaxSize = 65438; const secondPacketSize = 10; res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize)); }); return expect( fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer()) ).to.timeout; }); it('should not timeout on cloning response without consuming one of the streams when the second packet size is less than default highWaterMark', function () { // TODO: fix test. if (!isNodeLowerThan('v16.0.0')) { this.skip(); } this.timeout(300); const url = local.mockResponse(res => { const firstPacketMaxSize = 65438; const secondPacketSize = 16 * 1024; // = defaultHighWaterMark res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1)); }); return expect( fetch(url).then(res => res.clone().buffer()) ).not.to.timeout; }); it('should not timeout on cloning response without consuming one of the streams when the second packet size is less than custom highWaterMark', function () { // TODO: fix test. if (!isNodeLowerThan('v16.0.0')) { this.skip(); } this.timeout(300); const url = local.mockResponse(res => { const firstPacketMaxSize = 65438; const secondPacketSize = 10; res.end(crypto.randomBytes(firstPacketMaxSize + secondPacketSize - 1)); }); return expect( fetch(url, {highWaterMark: 10}).then(res => res.clone().buffer()) ).not.to.timeout; }); it('should not timeout on cloning response without consuming one of the streams when the response size is double the custom large highWaterMark - 1', function () { // TODO: fix test. if (!isNodeLowerThan('v16.0.0')) { this.skip(); } this.timeout(300); const url = local.mockResponse(res => { res.end(crypto.randomBytes((2 * 512 * 1024) - 1)); }); return expect( fetch(url, {highWaterMark: 512 * 1024}).then(res => res.clone().buffer()) ).not.to.timeout; }); it('should allow get all responses of a header', () => { const url = `${base}cookie`; return fetch(url).then(res => { const expected = 'a=1, b=1'; expect(res.headers.get('set-cookie')).to.equal(expected); expect(res.headers.get('Set-Cookie')).to.equal(expected); }); }); it('should return all headers using raw()', async () => { const url = `${base}cookie`; const res = await fetch(url); const expected = [ 'a=1', 'b=1' ]; expect(res.headers.raw()['set-cookie']).to.deep.equal(expected); }); it('should allow deleting header', () => { const url = `${base}cookie`; return fetch(url).then(res => { res.headers.delete('set-cookie'); expect(res.headers.get('set-cookie')).to.be.null; }); }); it('should send request with connection keep-alive if agent is provided', async () => { const url = `${base}inspect`; const options = { agent: new http.Agent({ keepAlive: true }) }; const res = await fetch(url, options); const json = await res.json(); expect(json.headers.connection).to.equal('keep-alive'); }); it('should support fetch with Request instance', async () => { const url = `${base}hello`; const request = new Request(url); const res = await fetch(request); expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); await res.arrayBuffer(); }); it('should support fetch with Node.js URL object', async () => { const url = `${base}hello`; const urlObject = new URL(url); const request = new Request(urlObject); const res = await fetch(request); expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); await res.arrayBuffer(); }); it('should support fetch with WHATWG URL object', async () => { const url = `${base}hello`; const urlObject = new URL(url); const request = new Request(urlObject); const res = await fetch(request); expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); }); it('should keep `?` sign in URL when no params are given', async () => { const url = `${base}question?`; const urlObject = new URL(url); const request = new Request(urlObject); const res = await fetch(request); expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); await res.arrayBuffer(); }); it('if params are given, do not modify anything', async () => { const url = `${base}question?a=1`; const urlObject = new URL(url); const request = new Request(urlObject); const res = await fetch(request); expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); }); it('should preserve the hash (#) symbol', async () => { const url = `${base}question?#`; const urlObject = new URL(url); const request = new Request(urlObject); const res = await fetch(request); expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); }); it('should support reading blob as text', async () => { const res = new Response('hello'); const blob = await res.blob(); const text = await blob.text(); expect(text).to.equal('hello'); }); it('should support reading blob as arrayBuffer', async () => { const res = await new Response('hello'); const blob = await res.blob(); const arrayBuffer = await blob.arrayBuffer(); const string = String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)); expect(string).to.equal('hello'); }); it('should support reading blob as stream', async () => { const blob = await new Response('hello').blob(); const str = await text(blob.stream()); expect(str).to.equal('hello'); }); it('should support blob round-trip', async () => { const helloUrl = `${base}hello`; const helloRes = await fetch(helloUrl); const helloBlob = await helloRes.blob(); const inspectUrl = `${base}inspect`; const {size, type} = helloBlob; const inspectRes = await fetch(inspectUrl, { method: 'POST', body: helloBlob }); const {body, headers} = await inspectRes.json(); expect(body).to.equal('world'); expect(headers['content-type']).to.equal(type); expect(headers['content-length']).to.equal(String(size)); }); it('should support overwrite Request instance', async () => { const url = `${base}inspect`; const request = new Request(url, { method: 'POST', headers: { a: '1' } }); const res = await fetch(request, { method: 'GET', headers: { a: '2' } }); const {method, headers} = await res.json(); expect(method).to.equal('GET'); expect(headers.a).to.equal('2'); }); it('should support arrayBuffer(), blob(), text(), json() and buffer() method in Body constructor', () => { const body = new Body('a=1'); expect(body).to.have.property('arrayBuffer'); expect(body).to.have.property('blob'); expect(body).to.have.property('text'); expect(body).to.have.property('json'); expect(body).to.have.property('buffer'); }); /* eslint-disable-next-line func-names */ it('should create custom FetchError', function funcName() { const systemError = new Error('system'); systemError.code = 'ESOMEERROR'; const err = new FetchError('test message', 'test-error', systemError); expect(err).to.be.an.instanceof(Error); expect(err).to.be.an.instanceof(FetchError); expect(err.name).to.equal('FetchError'); expect(err.message).to.equal('test message'); expect(err.type).to.equal('test-error'); expect(err.code).to.equal('ESOMEERROR'); expect(err.errno).to.equal('ESOMEERROR'); // Reading the stack is quite slow (~30-50ms) expect(err.stack).to.include('funcName').and.to.startWith(`${err.name}: ${err.message}`); }); it('should support https request', async function () { this.timeout(5000); const url = 'https://github.com/'; const options = { method: 'HEAD' }; const res = await fetch(url, options); expect(res.status).to.equal(200); expect(res.ok).to.be.true; }); // Issue #414 it('should reject if attempt to accumulate body stream throws', () => { const res = new Response(stream.Readable.from((async function * () { yield encoder.encode('tada'); await new Promise(resolve => { setTimeout(resolve, 200); }); yield {tada: 'yes'}; })())); return expect(res.text()).to.eventually.be.rejected .and.be.an.instanceOf(FetchError) .and.include({type: 'system'}) .and.have.property('message').that.include('Could not create Buffer'); }); it('supports supplying a lookup function to the agent', () => { const url = `${base}redirect/301`; let called = 0; function lookupSpy(hostname, options, callback) { called++; return lookup(hostname, options, callback); } const agent = http.Agent({lookup: lookupSpy}); return fetch(url, {agent}).then(() => { expect(called).to.equal(2); }); }); it('supports supplying a famliy option to the agent', async () => { const url = `${base}redirect/301`; const families = []; const family = Symbol('family'); function lookupSpy(hostname, options, callback) { families.push(options.family); return lookup(hostname, {}, callback); } const agent = new http.Agent({lookup: lookupSpy, family}); const res = await fetch(url, {agent}); expect(families).to.have.length(2); expect(families[0]).to.equal(family); expect(families[1]).to.equal(family); await res.arrayBuffer(); }); it('should allow a function supplying the agent', async () => { const url = `${base}inspect`; const agent = new http.Agent({ keepAlive: true }); let parsedURL; const res = await fetch(url, { agent(_parsedURL) { parsedURL = _parsedURL; return agent; } }); const json = await res.json(); // The agent provider should have been called expect(parsedURL.protocol).to.equal('http:'); // The agent we returned should have been used expect(json.headers.connection).to.equal('keep-alive'); }); it('should calculate content length and extract content type for each body type', () => { const url = `${base}hello`; const bodyContent = 'a=1'; const streamBody = stream.Readable.from(bodyContent); const streamRequest = new Request(url, { method: 'POST', body: streamBody, size: 1024 }); const blobBody = new Blob([bodyContent], {type: 'text/plain'}); const blobRequest = new Request(url, { method: 'POST', body: blobBody, size: 1024 }); const formBody = new FormData(); formBody.append('a', '1'); const formRequest = new Request(url, { method: 'POST', body: formBody, size: 1024 }); const bufferBody = encoder.encode(bodyContent); const bufferRequest = new Request(url, { method: 'POST', body: bufferBody, size: 1024 }); const stringRequest = new Request(url, { method: 'POST', body: bodyContent, size: 1024 }); const nullRequest = new Request(url, { method: 'GET', body: null, size: 1024 }); expect(getTotalBytes(streamRequest)).to.be.null; expect(getTotalBytes(blobRequest)).to.equal(blobBody.size); expect(getTotalBytes(formRequest)).to.not.be.null; expect(getTotalBytes(bufferRequest)).to.equal(bufferBody.length); expect(getTotalBytes(stringRequest)).to.equal(bodyContent.length); expect(getTotalBytes(nullRequest)).to.equal(0); expect(extractContentType(streamBody)).to.be.null; expect(extractContentType(blobBody)).to.equal('text/plain'); expect(extractContentType(formBody)).to.startWith('multipart/form-data'); expect(extractContentType(bufferBody)).to.be.null; expect(extractContentType(bodyContent)).to.equal('text/plain;charset=UTF-8'); expect(extractContentType(null)).to.be.null; }); it('should encode URLs as UTF-8', async () => { const url = `${base}möbius`; const res = await fetch(url); expect(res.url).to.equal(`${base}m%C3%B6bius`); }); it('static Response.json should work', async () => { const response = Response.json({foo: 'bar'}); expect(response.status).to.equal(200); expect(response.headers.get('content-type')).to.equal('application/json'); expect(await response.text()).to.equal(JSON.stringify({foo: 'bar'})); const response1 = Response.json(null, { status: 301, statusText: 'node-fetch', headers: { 'Content-Type': 'text/plain' } }); expect(response1.headers.get('content-type')).to.equal('text/plain'); expect(response1.status).to.equal(301); expect(response1.statusText).to.equal('node-fetch'); const response2 = Response.json(null, { headers: { 'CoNtEnT-TypE': 'text/plain' } }); expect(response2.headers.get('content-type')).to.equal('text/plain'); }); }); describe('node-fetch using IPv6', () => { const local = new TestServer('[::1]'); let base; before(async () => { await local.start(); base = `http://${local.hostname}:${local.port}/`; }); after(async () => { return local.stop(); }); it('should resolve into response', async () => { const url = `${base}hello`; expect(url).to.contain('[::1]'); const res = await fetch(url); expect(res).to.be.an.instanceof(Response); expect(res.headers).to.be.an.instanceof(Headers); expect(res.body).to.be.an.instanceof(stream.Transform); expect(res.bodyUsed).to.be.false; expect(res.url).to.equal(url); expect(res.ok).to.be.true; expect(res.status).to.equal(200); expect(res.statusText).to.equal('OK'); }); }); node-fetch-3.3.2/test/referrer.js000066400000000000000000000454401445773333000167040ustar00rootroot00000000000000import chai from 'chai'; import fetch, {Request, Headers} from '../src/index.js'; import { DEFAULT_REFERRER_POLICY, ReferrerPolicy, stripURLForUseAsAReferrer, validateReferrerPolicy, isOriginPotentiallyTrustworthy, isUrlPotentiallyTrustworthy, determineRequestsReferrer, parseReferrerPolicyFromHeader } from '../src/utils/referrer.js'; import TestServer from './utils/server.js'; const {expect} = chai; describe('fetch() with referrer and referrerPolicy', () => { const local = new TestServer(); let base; before(async () => { await local.start(); base = `http://${local.hostname}:${local.port}/`; }); after(async () => { return local.stop(); }); it('should send request without a referrer by default', () => { return fetch(`${base}inspect`).then(res => res.json()).then(res => { expect(res.headers.referer).to.be.undefined; }); }); it('should send request with a referrer', () => { return fetch(`${base}inspect`, { referrer: base, referrerPolicy: 'unsafe-url' }).then(res => res.json()).then(res => { expect(res.headers.referer).to.equal(base); }); }); it('should send request with referrerPolicy strict-origin-when-cross-origin by default', () => { return Promise.all([ fetch(`${base}inspect`, { referrer: base }).then(res => res.json()).then(res => { expect(res.headers.referer).to.equal(base); }), fetch(`${base}inspect`, { referrer: 'https://example.com' }).then(res => res.json()).then(res => { expect(res.headers.referer).to.be.undefined; }) ]); }); it('should send request with a referrer and respect redirected referrer-policy', () => { return Promise.all([ fetch(`${base}redirect/referrer-policy`, { referrer: base }).then(res => res.json()).then(res => { expect(res.headers.referer).to.equal(base); }), fetch(`${base}redirect/referrer-policy`, { referrer: 'https://example.com' }).then(res => res.json()).then(res => { expect(res.headers.referer).to.be.undefined; }), fetch(`${base}redirect/referrer-policy`, { referrer: 'https://example.com', referrerPolicy: 'unsafe-url' }).then(res => res.json()).then(res => { expect(res.headers.referer).to.equal('https://example.com/'); }), fetch(`${base}redirect/referrer-policy/same-origin`, { referrer: 'https://example.com', referrerPolicy: 'unsafe-url' }).then(res => res.json()).then(res => { expect(res.headers.referer).to.undefined; }) ]); }); }); describe('Request constructor', () => { describe('referrer', () => { it('should leave referrer undefined by default', () => { const req = new Request('http://example.com'); expect(req.referrer).to.be.undefined; }); it('should accept empty string referrer as no-referrer', () => { const referrer = ''; const req = new Request('http://example.com', {referrer}); expect(req.referrer).to.equal(referrer); }); it('should accept about:client referrer as client', () => { const referrer = 'about:client'; const req = new Request('http://example.com', {referrer}); expect(req.referrer).to.equal(referrer); }); it('should accept about://client referrer as client', () => { const req = new Request('http://example.com', {referrer: 'about://client'}); expect(req.referrer).to.equal('about:client'); }); it('should accept a string URL referrer', () => { const referrer = 'http://example.com/'; const req = new Request('http://example.com', {referrer}); expect(req.referrer).to.equal(referrer); }); it('should accept a URL referrer', () => { const referrer = new URL('http://example.com'); const req = new Request('http://example.com', {referrer}); expect(req.referrer).to.equal(referrer.toString()); }); it('should accept a referrer from input', () => { const referrer = 'http://example.com/'; const req = new Request(new Request('http://example.com', {referrer})); expect(req.referrer).to.equal(referrer.toString()); }); it('should throw a TypeError for an invalid URL', () => { expect(() => { const req = new Request('http://example.com', {referrer: 'foobar'}); expect.fail(req); }).to.throw(TypeError, /Invalid URL/); }); }); describe('referrerPolicy', () => { it('should default refererPolicy to empty string', () => { const req = new Request('http://example.com'); expect(req.referrerPolicy).to.equal(''); }); it('should accept refererPolicy', () => { const referrerPolicy = 'unsafe-url'; const req = new Request('http://example.com', {referrerPolicy}); expect(req.referrerPolicy).to.equal(referrerPolicy); }); it('should accept referrerPolicy from input', () => { const referrerPolicy = 'unsafe-url'; const req = new Request(new Request('http://example.com', {referrerPolicy})); expect(req.referrerPolicy).to.equal(referrerPolicy); }); it('should throw a TypeError for an invalid referrerPolicy', () => { expect(() => { const req = new Request('http://example.com', {referrerPolicy: 'foobar'}); expect.fail(req); }).to.throw(TypeError, 'Invalid referrerPolicy: foobar'); }); }); }); describe('utils/referrer', () => { it('default policy should be strict-origin-when-cross-origin', () => { expect(DEFAULT_REFERRER_POLICY).to.equal('strict-origin-when-cross-origin'); }); describe('stripURLForUseAsAReferrer', () => { it('should return no-referrer for null/undefined URL', () => { expect(stripURLForUseAsAReferrer(undefined)).to.equal('no-referrer'); expect(stripURLForUseAsAReferrer(null)).to.equal('no-referrer'); }); it('should return no-referrer for about:, blob:, and data: URLs', () => { expect(stripURLForUseAsAReferrer('about:client')).to.equal('no-referrer'); expect(stripURLForUseAsAReferrer('blob:theblog')).to.equal('no-referrer'); expect(stripURLForUseAsAReferrer('data:,thedata')).to.equal('no-referrer'); }); it('should strip the username, password, and hash', () => { const urlStr = 'http://foo:bar@example.com/foo?q=search#theanchor'; expect(stripURLForUseAsAReferrer(urlStr).toString()) .to.equal('http://example.com/foo?q=search'); }); it('should strip the pathname and query when origin-only', () => { const urlStr = 'http://foo:bar@example.com/foo?q=search#theanchor'; expect(stripURLForUseAsAReferrer(urlStr, true).toString()) .to.equal('http://example.com/'); }); }); describe('validateReferrerPolicy', () => { it('should return the referrer policy', () => { for (const referrerPolicy of ReferrerPolicy) { expect(validateReferrerPolicy(referrerPolicy)).to.equal(referrerPolicy); } }); it('should throw a TypeError for invalid referrer policies', () => { expect(validateReferrerPolicy.bind(null, undefined)) .to.throw(TypeError, 'Invalid referrerPolicy: undefined'); expect(validateReferrerPolicy.bind(null, null)) .to.throw(TypeError, 'Invalid referrerPolicy: null'); expect(validateReferrerPolicy.bind(null, false)) .to.throw(TypeError, 'Invalid referrerPolicy: false'); expect(validateReferrerPolicy.bind(null, 0)) .to.throw(TypeError, 'Invalid referrerPolicy: 0'); expect(validateReferrerPolicy.bind(null, 'always')) .to.throw(TypeError, 'Invalid referrerPolicy: always'); }); }); const testIsOriginPotentiallyTrustworthyStatements = func => { it('should be potentially trustworthy for HTTPS and WSS URLs', () => { expect(func(new URL('https://example.com'))).to.be.true; expect(func(new URL('wss://example.com'))).to.be.true; }); it('should be potentially trustworthy for loopback IP address URLs', () => { expect(func(new URL('http://127.0.0.1'))).to.be.true; expect(func(new URL('http://127.1.2.3'))).to.be.true; expect(func(new URL('ws://[::1]'))).to.be.true; }); it('should not be potentially trustworthy for "localhost" URLs', () => { expect(func(new URL('http://localhost'))).to.be.false; }); it('should be potentially trustworthy for file: URLs', () => { expect(func(new URL('file://foo/bar'))).to.be.true; }); it('should not be potentially trustworthy for all other origins', () => { expect(func(new URL('http://example.com'))).to.be.false; expect(func(new URL('ws://example.com'))).to.be.false; }); }; describe('isOriginPotentiallyTrustworthy', () => { testIsOriginPotentiallyTrustworthyStatements(isOriginPotentiallyTrustworthy); }); describe('isUrlPotentiallyTrustworthy', () => { it('should be potentially trustworthy for about:blank and about:srcdoc', () => { expect(isUrlPotentiallyTrustworthy(new URL('about:blank'))).to.be.true; expect(isUrlPotentiallyTrustworthy(new URL('about:srcdoc'))).to.be.true; }); it('should be potentially trustworthy for data: URLs', () => { expect(isUrlPotentiallyTrustworthy(new URL('data:,thedata'))).to.be.true; }); it('should be potentially trustworthy for blob: and filesystem: URLs', () => { expect(isUrlPotentiallyTrustworthy(new URL('blob:theblob'))).to.be.true; expect(isUrlPotentiallyTrustworthy(new URL('filesystem:thefilesystem'))).to.be.true; }); testIsOriginPotentiallyTrustworthyStatements(isUrlPotentiallyTrustworthy); }); describe('determineRequestsReferrer', () => { it('should return null for no-referrer or empty referrerPolicy', () => { expect(determineRequestsReferrer({referrer: 'no-referrer'})).to.be.null; expect(determineRequestsReferrer({referrerPolicy: ''})).to.be.null; }); it('should return no-referrer for about:client', () => { expect(determineRequestsReferrer({ referrer: 'about:client', referrerPolicy: DEFAULT_REFERRER_POLICY })).to.equal('no-referrer'); }); it('should return just the origin for URLs over 4096 characters', () => { expect(determineRequestsReferrer({ url: 'http://foo:bar@example.com/foo?q=search#theanchor', referrer: `http://example.com/${'0'.repeat(4096)}`, referrerPolicy: DEFAULT_REFERRER_POLICY }).toString()).to.equal('http://example.com/'); }); it('should alter the referrer URL by callback', () => { expect(determineRequestsReferrer({ url: 'http://foo:bar@example.com/foo?q=search#theanchor', referrer: 'http://foo:bar@example.com/foo?q=search#theanchor', referrerPolicy: 'unsafe-url' }, { referrerURLCallback: referrerURL => { return new URL(referrerURL.toString().replace(/^http:/, 'myprotocol:')); } }).toString()).to.equal('myprotocol://example.com/foo?q=search'); }); it('should alter the referrer origin by callback', () => { expect(determineRequestsReferrer({ url: 'http://foo:bar@example.com/foo?q=search#theanchor', referrer: 'http://foo:bar@example.com/foo?q=search#theanchor', referrerPolicy: 'origin' }, { referrerOriginCallback: referrerOrigin => { return new URL(referrerOrigin.toString().replace(/^http:/, 'myprotocol:')); } }).toString()).to.equal('myprotocol://example.com/'); }); it('should throw a TypeError for an invalid policy', () => { expect(() => { determineRequestsReferrer({ url: 'http://foo:bar@example.com/foo?q=search#theanchor', referrer: 'http://foo:bar@example.com/foo?q=search#theanchor', referrerPolicy: 'always' }); }).to.throw(TypeError, 'Invalid referrerPolicy: always'); }); const referrerPolicyTestLabel = ({currentURLTrust, referrerURLTrust, sameOrigin}) => { if (currentURLTrust === null && referrerURLTrust === null && sameOrigin === null) { return 'Always'; } const result = []; if (currentURLTrust !== null) { result.push(`Current URL is ${currentURLTrust ? '' : 'not '}potentially trustworthy`); } if (referrerURLTrust !== null) { result.push(`Referrer URL is ${referrerURLTrust ? '' : 'not '}potentially trustworthy`); } if (sameOrigin !== null) { result.push(`Current URL & Referrer URL do ${sameOrigin ? '' : 'not '}have same origin`); } return result.join(', '); }; const referrerPolicyTests = (referrerPolicy, matrix) => { describe(`Referrer policy: ${referrerPolicy}`, () => { for (const {currentURLTrust, referrerURLTrust, sameOrigin, result} of matrix) { describe(referrerPolicyTestLabel({currentURLTrust, referrerURLTrust, sameOrigin}), () => { const requests = []; if (sameOrigin === true || sameOrigin === null) { requests.push({ referrerPolicy, url: 'http://foo:bar@example.com/foo?q=search#theanchor', referrer: 'http://foo:bar@example.com/foo?q=search#theanchor' }); } if (sameOrigin === false || sameOrigin === null) { requests.push({ referrerPolicy, url: 'http://foo:bar@example2.com/foo?q=search#theanchor', referrer: 'http://foo:bar@example.com/foo?q=search#theanchor' }); } let requestsLength = requests.length; switch (currentURLTrust) { case null: for (let i = 0; i < requestsLength; i++) { const req = requests[i]; requests.push({...req, url: req.url.replace(/^http:/, 'https:')}); } break; case true: for (let i = 0; i < requestsLength; i++) { const req = requests[i]; req.url = req.url.replace(/^http:/, 'https:'); } break; case false: // nothing to do, default is not potentially trustworthy break; default: throw new TypeError(`Invalid currentURLTrust condition: ${currentURLTrust}`); } requestsLength = requests.length; switch (referrerURLTrust) { case null: for (let i = 0; i < requestsLength; i++) { const req = requests[i]; if (sameOrigin) { if (req.url.startsWith('https:')) { requests.splice(i, 1); } else { continue; } } requests.push({...req, referrer: req.referrer.replace(/^http:/, 'https:')}); } break; case true: for (let i = 0; i < requestsLength; i++) { const req = requests[i]; req.referrer = req.referrer.replace(/^http:/, 'https:'); } break; case false: // nothing to do, default is not potentially trustworthy break; default: throw new TypeError(`Invalid referrerURLTrust condition: ${referrerURLTrust}`); } it('should have tests', () => { expect(requests).to.not.be.empty; }); for (const req of requests) { it(`should return ${result} for url: ${req.url}, referrer: ${req.referrer}`, () => { if (result === 'no-referrer') { return expect(determineRequestsReferrer(req).toString()) .to.equal('no-referrer'); } if (result === 'referrer-origin') { const referrerOrigih = stripURLForUseAsAReferrer(req.referrer, true); return expect(determineRequestsReferrer(req).toString()) .to.equal(referrerOrigih.toString()); } if (result === 'referrer-url') { const referrerURL = stripURLForUseAsAReferrer(req.referrer); return expect(determineRequestsReferrer(req).toString()) .to.equal(referrerURL.toString()); } throw new TypeError(`Invalid result: ${result}`); }); } }); } }); }; // 3.1 no-referrer referrerPolicyTests('no-referrer', [ {currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'no-referrer'} ]); // 3.2 no-referrer-when-downgrade referrerPolicyTests('no-referrer-when-downgrade', [ {currentURLTrust: false, referrerURLTrust: true, sameOrigin: null, result: 'no-referrer'}, {currentURLTrust: null, referrerURLTrust: false, sameOrigin: null, result: 'referrer-url'}, {currentURLTrust: true, referrerURLTrust: true, sameOrigin: null, result: 'referrer-url'} ]); // 3.3 same-origin referrerPolicyTests('same-origin', [ {currentURLTrust: null, referrerURLTrust: null, sameOrigin: false, result: 'no-referrer'}, {currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'} ]); // 3.4 origin referrerPolicyTests('origin', [ {currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'referrer-origin'} ]); // 3.5 strict-origin referrerPolicyTests('strict-origin', [ {currentURLTrust: false, referrerURLTrust: true, sameOrigin: null, result: 'no-referrer'}, {currentURLTrust: null, referrerURLTrust: false, sameOrigin: null, result: 'referrer-origin'}, {currentURLTrust: true, referrerURLTrust: true, sameOrigin: null, result: 'referrer-origin'} ]); // 3.6 origin-when-cross-origin referrerPolicyTests('origin-when-cross-origin', [ {currentURLTrust: null, referrerURLTrust: null, sameOrigin: false, result: 'referrer-origin'}, {currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'} ]); // 3.7 strict-origin-when-cross-origin referrerPolicyTests('strict-origin-when-cross-origin', [ {currentURLTrust: false, referrerURLTrust: true, sameOrigin: false, result: 'no-referrer'}, {currentURLTrust: null, referrerURLTrust: false, sameOrigin: false, result: 'referrer-origin'}, {currentURLTrust: true, referrerURLTrust: true, sameOrigin: false, result: 'referrer-origin'}, {currentURLTrust: null, referrerURLTrust: null, sameOrigin: true, result: 'referrer-url'} ]); // 3.8 unsafe-url referrerPolicyTests('unsafe-url', [ {currentURLTrust: null, referrerURLTrust: null, sameOrigin: null, result: 'referrer-url'} ]); }); describe('parseReferrerPolicyFromHeader', () => { it('should return an empty string when no referrer policy is found', () => { expect(parseReferrerPolicyFromHeader(new Headers())).to.equal(''); expect(parseReferrerPolicyFromHeader( new Headers([['Referrer-Policy', '']]) )).to.equal(''); }); it('should return the last valid referrer policy', () => { expect(parseReferrerPolicyFromHeader( new Headers([['Referrer-Policy', 'no-referrer']]) )).to.equal('no-referrer'); expect(parseReferrerPolicyFromHeader( new Headers([['Referrer-Policy', 'no-referrer unsafe-url']]) )).to.equal('unsafe-url'); expect(parseReferrerPolicyFromHeader( new Headers([['Referrer-Policy', 'foo no-referrer bar']]) )).to.equal('no-referrer'); expect(parseReferrerPolicyFromHeader( new Headers([['Referrer-Policy', 'foo no-referrer unsafe-url bar']]) )).to.equal('unsafe-url'); }); it('should use all Referrer-Policy headers', () => { expect(parseReferrerPolicyFromHeader( new Headers([ ['Referrer-Policy', 'no-referrer'], ['Referrer-Policy', ''] ]) )).to.equal('no-referrer'); expect(parseReferrerPolicyFromHeader( new Headers([ ['Referrer-Policy', 'no-referrer'], ['Referrer-Policy', 'unsafe-url'] ]) )).to.equal('unsafe-url'); expect(parseReferrerPolicyFromHeader( new Headers([ ['Referrer-Policy', 'no-referrer foo'], ['Referrer-Policy', 'bar unsafe-url wow'] ]) )).to.equal('unsafe-url'); expect(parseReferrerPolicyFromHeader( new Headers([ ['Referrer-Policy', 'no-referrer unsafe-url'], ['Referrer-Policy', 'foo bar'] ]) )).to.equal('unsafe-url'); }); }); }); node-fetch-3.3.2/test/request.js000066400000000000000000000203461445773333000165560ustar00rootroot00000000000000import stream from 'node:stream'; import http from 'node:http'; import AbortController from 'abort-controller'; import chai from 'chai'; import FormData from 'form-data'; import Blob from 'fetch-blob'; import {Request} from '../src/index.js'; import TestServer from './utils/server.js'; const {expect} = chai; describe('Request', () => { const local = new TestServer(); let base; before(async () => { await local.start(); base = `http://${local.hostname}:${local.port}/`; }); after(async () => { return local.stop(); }); it('should have attributes conforming to Web IDL', () => { const request = new Request('https://github.com/'); const enumerableProperties = []; for (const property in request) { enumerableProperties.push(property); } for (const toCheck of [ 'body', 'bodyUsed', 'arrayBuffer', 'blob', 'json', 'text', 'method', 'url', 'headers', 'redirect', 'clone', 'signal' ]) { expect(enumerableProperties).to.contain(toCheck); } for (const toCheck of [ 'body', 'bodyUsed', 'method', 'url', 'headers', 'redirect', 'signal' ]) { expect(() => { request[toCheck] = 'abc'; }).to.throw(); } }); it('should support wrapping Request instance', () => { const url = `${base}hello`; const form = new FormData(); form.append('a', '1'); const {signal} = new AbortController(); const r1 = new Request(url, { method: 'POST', follow: 1, body: form, signal }); const r2 = new Request(r1, { follow: 2 }); expect(r2.url).to.equal(url); expect(r2.method).to.equal('POST'); expect(r2.signal).to.equal(signal); // Note that we didn't clone the body expect(r2.body).to.equal(form); expect(r1.follow).to.equal(1); expect(r2.follow).to.equal(2); expect(r1.counter).to.equal(0); expect(r2.counter).to.equal(0); }); it('should override signal on derived Request instances', () => { const parentAbortController = new AbortController(); const derivedAbortController = new AbortController(); const parentRequest = new Request(`${base}hello`, { signal: parentAbortController.signal }); const derivedRequest = new Request(parentRequest, { signal: derivedAbortController.signal }); expect(parentRequest.signal).to.equal(parentAbortController.signal); expect(derivedRequest.signal).to.equal(derivedAbortController.signal); }); it('should allow removing signal on derived Request instances', () => { const parentAbortController = new AbortController(); const parentRequest = new Request(`${base}hello`, { signal: parentAbortController.signal }); const derivedRequest = new Request(parentRequest, { signal: null }); expect(parentRequest.signal).to.equal(parentAbortController.signal); expect(derivedRequest.signal).to.equal(null); }); it('should throw error with GET/HEAD requests with body', () => { expect(() => new Request(base, {body: ''})) .to.throw(TypeError); expect(() => new Request(base, {body: 'a'})) .to.throw(TypeError); expect(() => new Request(base, {body: '', method: 'HEAD'})) .to.throw(TypeError); expect(() => new Request(base, {body: 'a', method: 'HEAD'})) .to.throw(TypeError); expect(() => new Request(base, {body: 'a', method: 'get'})) .to.throw(TypeError); expect(() => new Request(base, {body: 'a', method: 'head'})) .to.throw(TypeError); expect(() => new Request(new Request(base), {body: 'a'})) .to.throw(TypeError); }); it('should throw error when including credentials', () => { expect(() => new Request('https://john:pass@github.com/')) .to.throw(TypeError); expect(() => new Request(new URL('https://john:pass@github.com/'))) .to.throw(TypeError); }); it('should default to null as body', () => { const request = new Request(base); expect(request.body).to.equal(null); return request.text().then(result => expect(result).to.equal('')); }); it('should support parsing headers', () => { const url = base; const request = new Request(url, { headers: { a: '1' } }); expect(request.url).to.equal(url); expect(request.headers.get('a')).to.equal('1'); }); it('should uppercase DELETE, GET, HEAD, OPTIONS, POST and PUT methods', () => { const url = base; for (const method of ['DELETE', 'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT']) { const request = new Request(url, { method: method.toLowerCase() }); expect(request.method).to.equal(method); } }); it('should not uppercase unknown methods and patch', () => { const url = base; for (const method of ['patch', 'chicken']) { const request = new Request(url, {method}); expect(request.method).to.equal(method); } }); it('should support arrayBuffer() method', () => { const url = base; const request = new Request(url, { method: 'POST', body: 'a=1' }); expect(request.url).to.equal(url); return request.arrayBuffer().then(result => { expect(result).to.be.an.instanceOf(ArrayBuffer); const string = String.fromCharCode.apply(null, new Uint8Array(result)); expect(string).to.equal('a=1'); }); }); it('should support text() method', () => { const url = base; const request = new Request(url, { method: 'POST', body: 'a=1' }); expect(request.url).to.equal(url); return request.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support json() method', () => { const url = base; const request = new Request(url, { method: 'POST', body: '{"a":1}' }); expect(request.url).to.equal(url); return request.json().then(result => { expect(result.a).to.equal(1); }); }); it('should support buffer() method', () => { const url = base; const request = new Request(url, { method: 'POST', body: 'a=1' }); expect(request.url).to.equal(url); return request.buffer().then(result => { expect(result.toString()).to.equal('a=1'); }); }); it('should support blob() method', async () => { const url = base; const request = new Request(url, { method: 'POST', body: new TextEncoder().encode('a=1') }); expect(request.url).to.equal(url); const blob = await request.blob(); expect(blob).to.be.an.instanceOf(Blob); expect(blob.size).to.equal(3); expect(blob.type).to.equal(''); }); it('should support clone() method', () => { const url = base; const body = stream.Readable.from('a=1'); const agent = new http.Agent(); const {signal} = new AbortController(); const request = new Request(url, { body, method: 'POST', redirect: 'manual', headers: { b: '2' }, follow: 3, compress: false, agent, signal }); const cl = request.clone(); expect(cl.url).to.equal(url); expect(cl.method).to.equal('POST'); expect(cl.redirect).to.equal('manual'); expect(cl.headers.get('b')).to.equal('2'); expect(cl.follow).to.equal(3); expect(cl.compress).to.equal(false); expect(cl.method).to.equal('POST'); expect(cl.counter).to.equal(0); expect(cl.agent).to.equal(agent); expect(cl.signal).to.equal(signal); // Clone body shouldn't be the same body expect(cl.body).to.not.equal(body); return Promise.all([cl.text(), request.text()]).then(results => { expect(results[0]).to.equal('a=1'); expect(results[1]).to.equal('a=1'); }); }); it('should support ArrayBuffer as body', () => { const encoder = new TextEncoder(); const request = new Request(base, { method: 'POST', body: encoder.encode('a=1').buffer }); return request.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support Uint8Array as body', () => { const encoder = new TextEncoder(); const request = new Request(base, { method: 'POST', body: encoder.encode('a=1') }); return request.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support DataView as body', () => { const encoder = new TextEncoder(); const request = new Request(base, { method: 'POST', body: new DataView(encoder.encode('a=1').buffer) }); return request.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should warn once when using .data (request)', () => new Promise(resolve => { process.once('warning', evt => { expect(evt.message).to.equal('.data is not a valid RequestInit property, use .body instead'); resolve(); }); // eslint-disable-next-line no-new new Request(base, { data: '' }); })); }); node-fetch-3.3.2/test/response.js000066400000000000000000000151351445773333000167240ustar00rootroot00000000000000import * as stream from 'node:stream'; import chai from 'chai'; import {Response, Blob} from '../src/index.js'; import TestServer from './utils/server.js'; const {expect} = chai; describe('Response', () => { const local = new TestServer(); let base; before(async () => { await local.start(); base = `http://${local.hostname}:${local.port}/`; }); after(async () => { return local.stop(); }); it('should have attributes conforming to Web IDL', () => { const res = new Response(); const enumerableProperties = []; for (const property in res) { enumerableProperties.push(property); } for (const toCheck of [ 'body', 'bodyUsed', 'arrayBuffer', 'blob', 'json', 'text', 'type', 'url', 'status', 'ok', 'redirected', 'statusText', 'headers', 'clone' ]) { expect(enumerableProperties).to.contain(toCheck); } for (const toCheck of [ 'body', 'bodyUsed', 'type', 'url', 'status', 'ok', 'redirected', 'statusText', 'headers' ]) { expect(() => { res[toCheck] = 'abc'; }).to.throw(); } }); it('should support empty options', () => { const res = new Response(stream.Readable.from('a=1')); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support parsing headers', () => { const res = new Response(null, { headers: { a: '1' } }); expect(res.headers.get('a')).to.equal('1'); }); it('should decode responses containing BOM to json', async () => { const json = await new Response('\uFEFF{"a":1}').json(); expect(json.a).to.equal(1); }); it('should decode responses containing BOM to text', async () => { const text = await new Response('\uFEFF{"a":1}').text(); expect(text).to.equal('{"a":1}'); }); it('should keep BOM when getting raw bytes', async () => { const ab = await new Response('\uFEFF{"a":1}').arrayBuffer(); expect(ab.byteLength).to.equal(10); }); it('should support text() method', () => { const res = new Response('a=1'); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support json() method', () => { const res = new Response('{"a":1}'); return res.json().then(result => { expect(result.a).to.equal(1); }); }); it('should support buffer() method', () => { const res = new Response('a=1'); return res.buffer().then(result => { expect(result.toString()).to.equal('a=1'); }); }); it('should support blob() method', () => { const res = new Response('a=1', { method: 'POST', headers: { 'Content-Type': 'text/plain' } }); return res.blob().then(result => { expect(result).to.be.an.instanceOf(Blob); expect(result.size).to.equal(3); expect(result.type).to.equal('text/plain'); }); }); it('should support clone() method', () => { const body = stream.Readable.from('a=1'); const res = new Response(body, { headers: { a: '1' }, url: base, status: 346, statusText: 'production', highWaterMark: 789 }); const cl = res.clone(); expect(cl.headers.get('a')).to.equal('1'); expect(cl.type).to.equal('default'); expect(cl.url).to.equal(base); expect(cl.status).to.equal(346); expect(cl.statusText).to.equal('production'); expect(cl.highWaterMark).to.equal(789); expect(cl.ok).to.be.false; // Clone body shouldn't be the same body expect(cl.body).to.not.equal(body); return cl.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support stream as body', () => { const body = stream.Readable.from('a=1'); const res = new Response(body); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support string as body', () => { const res = new Response('a=1'); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support ArrayBuffer as body', () => { const encoder = new TextEncoder(); const res = new Response(encoder.encode('a=1')); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support blob as body', async () => { const res = new Response(new Blob(['a=1'])); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support Uint8Array as body', () => { const encoder = new TextEncoder(); const res = new Response(encoder.encode('a=1')); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should support DataView as body', () => { const encoder = new TextEncoder(); const res = new Response(new DataView(encoder.encode('a=1').buffer)); return res.text().then(result => { expect(result).to.equal('a=1'); }); }); it('should default to null as body', () => { const res = new Response(); expect(res.body).to.equal(null); return res.text().then(result => expect(result).to.equal('')); }); it('should default to 200 as status code', () => { const res = new Response(null); expect(res.status).to.equal(200); }); it('should default to empty string as url', () => { const res = new Response(); expect(res.url).to.equal(''); }); it('should cast string to stream using res.body', () => { const res = new Response('hi'); expect(res.body).to.be.an.instanceof(stream.Readable); }); it('should cast typed array to stream using res.body', () => { const res = new Response(Uint8Array.from([97])); expect(res.body).to.be.an.instanceof(stream.Readable); }); it('should cast blob to stream using res.body', () => { const res = new Response(new Blob(['a'])); expect(res.body).to.be.an.instanceof(stream.Readable); }); it('should not cast null to stream using res.body', () => { const res = new Response(null); expect(res.body).to.be.null; }); it('should cast typed array to text using res.text()', async () => { const res = new Response(Uint8Array.from([97])); expect(await res.text()).to.equal('a'); }); it('should cast stream to text using res.text() in a roundabout way', async () => { const {body} = new Response('a'); expect(body).to.be.an.instanceof(stream.Readable); const res = new Response(body); expect(await res.text()).to.equal('a'); }); it('should support error() static method', () => { const res = Response.error(); expect(res).to.be.an.instanceof(Response); expect(res.type).to.equal('error'); expect(res.status).to.equal(0); expect(res.statusText).to.equal(''); }); it('should warn once when using .data (response)', () => new Promise(resolve => { process.once('warning', evt => { expect(evt.message).to.equal('data doesn\'t exist, use json(), text(), arrayBuffer(), or body instead'); resolve(); }); new Response('a').data; })); }); node-fetch-3.3.2/test/utils/000077500000000000000000000000001445773333000156635ustar00rootroot00000000000000node-fetch-3.3.2/test/utils/chai-timeout.js000066400000000000000000000006151445773333000206130ustar00rootroot00000000000000import pTimeout from 'p-timeout'; export default ({Assertion}, utils) => { utils.addProperty(Assertion.prototype, 'timeout', async function () { let timeouted = false; await pTimeout(this._obj, 150, () => { timeouted = true; }); return this.assert( timeouted, 'expected promise to timeout but it was resolved', 'expected promise not to timeout but it timed out' ); }); }; node-fetch-3.3.2/test/utils/dummy.txt000066400000000000000000000000141445773333000175520ustar00rootroot00000000000000i am a dummynode-fetch-3.3.2/test/utils/server.js000066400000000000000000000255241445773333000175370ustar00rootroot00000000000000import http from 'node:http'; import zlib from 'node:zlib'; import {once} from 'node:events'; import busboy from 'busboy'; export default class TestServer { constructor(hostname) { this.server = http.createServer(this.router); // Node 8 default keepalive timeout is 5000ms // make it shorter here as we want to close server quickly at the end of tests this.server.keepAliveTimeout = 1000; this.server.on('error', err => { console.log(err.stack); }); this.server.on('connection', socket => { socket.setTimeout(1500); }); this.hostname = hostname || 'localhost'; } async start() { let host = this.hostname; if (host.startsWith('[')) { // If we're trying to listen on an IPv6 literal hostname, strip the // square brackets before binding to the IPv6 address host = host.slice(1, -1); } this.server.listen(0, host); return once(this.server, 'listening'); } async stop() { this.server.close(); return once(this.server, 'close'); } get port() { return this.server.address().port; } mockResponse(responseHandler) { this.server.nextResponseHandler = responseHandler; return `http://${this.hostname}:${this.port}/mocked`; } router(request, res) { const p = request.url; if (p === '/mocked') { if (this.nextResponseHandler) { this.nextResponseHandler(res); this.nextResponseHandler = undefined; } else { throw new Error('No mocked response. Use ’TestServer.mockResponse()’.'); } } if (p === '/hello') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('world'); } if (p.includes('question')) { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('ok'); } if (p === '/plain') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('text'); } if (p === '/no-status-text') { res.writeHead(200, '', {}).end(); } if (p === '/options') { res.statusCode = 200; res.setHeader('Allow', 'GET, HEAD, OPTIONS'); res.end('hello world'); } if (p === '/html') { res.statusCode = 200; res.setHeader('Content-Type', 'text/html'); res.end(''); } if (p === '/json') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify({ name: 'value' })); } if (p === '/gzip') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); zlib.gzip('hello world', (err, buffer) => { if (err) { throw err; } res.end(buffer); }); } if (p === '/gzip-truncated') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); zlib.gzip('hello world', (err, buffer) => { if (err) { throw err; } // Truncate the CRC checksum and size check at the end of the stream res.end(buffer.slice(0, -8)); }); } if (p === '/gzip-capital') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'GZip'); zlib.gzip('hello world', (err, buffer) => { if (err) { throw err; } res.end(buffer); }); } if (p === '/deflate') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'deflate'); zlib.deflate('hello world', (err, buffer) => { if (err) { throw err; } res.end(buffer); }); } if (p === '/brotli') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); if (typeof zlib.createBrotliDecompress === 'function') { res.setHeader('Content-Encoding', 'br'); zlib.brotliCompress('hello world', (err, buffer) => { if (err) { throw err; } res.end(buffer); }); } } if (p === '/deflate-raw') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'deflate'); zlib.deflateRaw('hello world', (err, buffer) => { if (err) { throw err; } res.end(buffer); }); } if (p === '/empty/deflate') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'deflate'); res.end(); } if (p === '/sdch') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'sdch'); res.end('fake sdch string'); } if (p === '/invalid-content-encoding') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.setHeader('Content-Encoding', 'gzip'); res.end('fake gzip string'); } if (p === '/timeout') { setTimeout(() => { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('text'); }, 1000); } if (p === '/slow') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.write('test'); setTimeout(() => { res.end('test'); }, 1000); } if (p === '/cookie') { res.statusCode = 200; res.setHeader('Set-Cookie', ['a=1', 'b=1']); res.end('cookie'); } if (p === '/size/chunk') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); setTimeout(() => { res.write('test'); }, 10); setTimeout(() => { res.end('test'); }, 20); } if (p === '/size/long') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('testtest'); } if (p === '/redirect/301') { res.statusCode = 301; res.setHeader('Location', '/inspect'); res.end(); } if (p === '/redirect/301/invalid') { res.statusCode = 301; res.setHeader('Location', '//super:invalid:url%/'); res.end(); } if (p.startsWith('/redirect-to/3')) { res.statusCode = p.slice(13, 16); res.setHeader('Location', p.slice(17)); res.end(); } if (p === '/redirect/301/otherhost') { res.statusCode = 301; res.setHeader('Location', 'https://github.com/node-fetch'); res.end(); } if (p === '/redirect/302') { res.statusCode = 302; res.setHeader('Location', '/inspect'); res.end(); } if (p === '/redirect/303') { res.statusCode = 303; res.setHeader('Location', '/inspect'); res.end(); } if (p === '/redirect/307') { res.statusCode = 307; res.setHeader('Location', '/inspect'); res.end(); } if (p === '/redirect/308') { res.statusCode = 308; res.setHeader('Location', '/inspect'); res.end(); } if (p === '/redirect/chain') { res.statusCode = 301; res.setHeader('Location', '/redirect/301'); res.end(); } if (p === '/redirect/no-location') { res.statusCode = 301; res.end(); } if (p === '/redirect/slow') { res.statusCode = 301; res.setHeader('Location', '/redirect/301'); setTimeout(() => { res.end(); }, 1000); } if (p === '/redirect/slow-chain') { res.statusCode = 301; res.setHeader('Location', '/redirect/slow'); setTimeout(() => { res.end(); }, 10); } if (p === '/redirect/slow-stream') { res.statusCode = 301; res.setHeader('Location', '/slow'); res.end(); } if (p === '/redirect/bad-location') { res.socket.write('HTTP/1.1 301\r\nLocation: <>\r\nContent-Length: 0\r\n'); res.socket.end('\r\n'); } if (p === '/redirect/referrer-policy') { res.statusCode = 301; res.setHeader('Location', '/inspect'); res.setHeader('Referrer-Policy', 'foo unsafe-url bar'); res.end(); } if (p === '/redirect/referrer-policy/same-origin') { res.statusCode = 301; res.setHeader('Location', '/inspect'); res.setHeader('Referrer-Policy', 'foo unsafe-url same-origin bar'); res.end(); } if (p === '/redirect/chunked') { res.writeHead(301, { Location: '/inspect', 'Transfer-Encoding': 'chunked' }); setTimeout(() => res.end(), 10); } if (p === '/error/400') { res.statusCode = 400; res.setHeader('Content-Type', 'text/plain'); res.end('client error'); } if (p === '/error/404') { res.statusCode = 404; res.setHeader('Content-Encoding', 'gzip'); res.end(); } if (p === '/error/500') { res.statusCode = 500; res.setHeader('Content-Type', 'text/plain'); res.end('server error'); } if (p === '/error/reset') { res.destroy(); } if (p === '/error/premature') { res.writeHead(200, {'content-length': 50}); res.write('foo'); setTimeout(() => { res.destroy(); }, 100); } if (p === '/error/premature/chunked') { res.writeHead(200, { 'Content-Type': 'application/json', 'Transfer-Encoding': 'chunked' }); res.write(`${JSON.stringify({data: 'hi'})}\n`); setTimeout(() => { res.write(`${JSON.stringify({data: 'bye'})}\n`); }, 200); setTimeout(() => { res.destroy(); }, 400); } if (p === '/chunked/split-ending') { res.socket.write('HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n'); res.socket.write('3\r\nfoo\r\n3\r\nbar\r\n'); setTimeout(() => { res.socket.write('0\r\n'); }, 10); setTimeout(() => { res.socket.end('\r\n'); }, 20); } if (p === '/chunked/multiple-ending') { res.socket.write('HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n'); res.socket.write('3\r\nfoo\r\n3\r\nbar\r\n0\r\n\r\n'); } if (p === '/error/json') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); res.end('invalid json'); } if (p === '/no-content') { res.statusCode = 204; res.end(); } if (p === '/no-content/gzip') { res.statusCode = 204; res.setHeader('Content-Encoding', 'gzip'); res.end(); } if (p === '/no-content/brotli') { res.statusCode = 204; res.setHeader('Content-Encoding', 'br'); res.end(); } if (p === '/not-modified') { res.statusCode = 304; res.end(); } if (p === '/not-modified/gzip') { res.statusCode = 304; res.setHeader('Content-Encoding', 'gzip'); res.end(); } if (p === '/inspect') { res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); let body = ''; request.on('data', c => { body += c; }); request.on('end', () => { res.end(JSON.stringify({ method: request.method, url: request.url, headers: request.headers, body })); }); } if (p === '/multipart') { let body = ''; res.statusCode = 200; res.setHeader('Content-Type', 'application/json'); const bb = busboy({headers: request.headers}); bb.on('file', async (fieldName, file, info) => { body += `${fieldName}=${info.filename}`; // consume file data // eslint-disable-next-line no-empty, no-unused-vars for await (const c of file) {} }); bb.on('field', (fieldName, value) => { body += `${fieldName}=${value}`; }); bb.on('close', () => { res.end(JSON.stringify({ method: request.method, url: request.url, headers: request.headers, body })); }); request.pipe(bb); } if (p === '/m%C3%B6bius') { res.statusCode = 200; res.setHeader('Content-Type', 'text/plain'); res.end('ok'); } } }