├── .editorconfig
├── .eslintrc.js
├── .gitignore
├── .npmignore
├── .travis.yml
├── .vscode
├── extensions.json
└── settings.json
├── LICENSE.md
├── README.md
├── browser.js
├── doc
└── shadow-fetch.png
├── examples
└── next.js
│ ├── package.json
│ ├── pages
│ └── index.js
│ └── server.js
├── index.js
├── lib
├── headers.js
├── incomingmessage.js
├── response.js
├── serverresponse.js
└── statustexts.js
├── package-lock.json
├── package.json
├── shadow-fetch-express
├── .npmignore
├── LICENSE.md
├── README.md
├── index.js
└── package.json
└── test
├── helper.js
├── test-benchmark.js
├── test-express.js
├── test-headers.js
├── test-incomingmessage.js
├── test-server.js
├── test-serverresponse.js
└── test-shadow-fetch.js
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | end_of_line = lf
5 | charset = utf-8
6 | trim_trailing_whitespace = true
7 | insert_final_newline = true
8 |
9 | [*.{json,js,jsx,html,css}]
10 | indent_size = 4
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "es6": true
6 | },
7 | "parserOptions": {
8 | "ecmaVersion": 2017
9 | },
10 | "extends": "eslint:recommended",
11 | "rules": {
12 | "indent": ["error", 4],
13 | "quotes": ["error", "double"],
14 | "semi": ["error", "always"],
15 | "no-console": 0
16 | }
17 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | .nyc_output
4 | examples/next.js/.next/
5 | examples/next.js/package-lock.json
6 | package
7 | *.tgz
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | doc/*.png
2 | test
3 | examples
4 | node_modules
5 | shadow-fetch-express
6 | .nyc_output
7 | .vscode
8 | coverage
9 | *.tgz
10 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - "node"
4 | - "lts/*"
5 | - "7"
6 | - "8"
7 | - "9"
8 | after_success: npm run coverage
9 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": [
3 | "dbaeumer.vscode-eslint",
4 | "michelemelluso.gitignore",
5 | "EditorConfig.EditorConfig",
6 | "flowtype.flow-for-vscode"
7 | ]
8 | }
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "workbench.editor.enablePreview": false,
3 | "editor.formatOnSave": false,
4 | "eslint.autoFixOnSave": true,
5 | "javascript.validate.enable": false,
6 | "flow.useNPMPackagedFlow": true,
7 | "typescript.disableAutomaticTypeAcquisition": true,
8 | "git.ignoreLimitWarning": true
9 | }
10 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | ## MIT License
2 |
3 | Copyright © 2018 Yoshiki Shibukawa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a
6 | copy of this software and associated documentation files (the "Software"),
7 | to deal in the Software without restriction, including without limitation
8 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 | and/or sell copies of the Software, and to permit persons to whom the
10 | Software is furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 | DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # shadow-fetch
2 |
3 | [](https://travis-ci.org/shibukawa/shadow-fetch)
4 | [](https://badge.fury.io/js/shadow-fetch)
5 | [](https://codecov.io/gh/shibukawa/shadow-fetch)
6 | [](https://snyk.io/test/npm/shadow-fetch)
7 | [](https://nodei.co/npm/shadow-fetch/)
8 |
9 | Accelorator of Server Side Rendering (and unit tests).
10 |
11 | Next.js and Nuxt.js and some framework improves productivity of development.
12 | You just write code once, the code run on the server and the client.
13 | Almost all part or code is compatible including server access code.
14 |
15 | Next.js's documents uses [isomorphic-unfetch](https://github.com/developit/unfetch/tree/master/packages/isomorphic-unfetch) and Nuxt.js's document uses [Axios](https://github.com/axios/axios).
16 | They are both excellent isomorphic libraries, but they make actual packet even if the client and the server work on the same process for server side rendering.
17 |
18 | This library provides ``fetch()`` compatible function and Node.js' ``http.createServer()`` compatible function.
19 | They are connected directly and bypass system calls.
20 |
21 | * You can make shorten SSR response a little
22 | * The request object has special attribute to identify actual access or shortcut access. You can skip authentication of BFF's API during SSR safely.
23 | * shadow-fetch provides special method to handle JSON. That method skip converting JSON into string.
24 |
25 | 
26 |
27 | ## Simple Benchmark
28 |
29 | | | time |
30 | |:-----------:|:-----------|
31 | | ``node-fetch`` and ``http.Server`` | 14.3 mS |
32 | | ``shadow-fetch`` with standard API | 1.8 mS |
33 | | ``shadow-fetch`` with direct JSON API | 0.13mS |
34 |
35 | * 8th Gen Core i5 with Node 9.4.0.
36 | * You can test via ``node run benchmark``.
37 |
38 | ## Installation
39 |
40 | ```sh
41 | $ npm install shadow-fetch
42 | ```
43 |
44 | ## Usage
45 |
46 | ### Node.js's http server
47 |
48 | shadow-fetch provides the function that is compatible with ``http.createServer()``. shadow-fetch's server is available inside the same process. So useally you should launch two servers.
49 |
50 | ```js
51 | const { createServer } = require("shadow-fetch");
52 | const { http } = require("http");
53 |
54 | handler = (req, res) => {
55 | res.writeHead(200, { "Content-Type": "application/json" });
56 | res.end(JSON.stringify({ message: "hello" }));
57 | };
58 |
59 | const server = createServer(handler);
60 | server.listen();
61 |
62 | const server = http.createServer(handler);
63 | server.listen(80);
64 | ```
65 |
66 | You can use ``fetch`` function to access this server:
67 |
68 | ```js
69 | import { fetch } from "shadow-fetch";
70 |
71 | const res = await fetch("/test");
72 |
73 | if (res.ok) {
74 | const json = await res.json();
75 | console.log(json.message);
76 | }
77 | ```
78 |
79 | ### Express.js
80 |
81 | Express.js modifies request object (replace prototype). shadow-fetch's middleware for Express.js enable shadow-fetch's feature even if you uses Express.js
82 |
83 | You should pass ``express()`` result to ``createServer()`` instead of ``app.listen()``. That uses Node.js's ``createServer()`` internally,
84 |
85 | ```js
86 | const { createServer } = require("shadow-fetch");
87 | const { shadowFetchMiddleware } = require("shadow-fetch-express");
88 |
89 | const app = express();
90 | app.use(shadowFetchMiddleware);
91 | app.use(bodyParser.json());
92 | app.post("/test", (req, res) => {
93 | t.is(req.shadow, true);
94 | t.is(req.body.message, "hello");
95 | res.send({ message: "world" });
96 | });
97 | const { fetch, createServer } = initFetch();
98 |
99 | createServer(app);
100 | ```
101 |
102 | ### Next.js
103 |
104 | It is alomot as same as Express.js. This package provides factory function that makes ``fetch`` and ``createServer()`` pairs. But they are not working on Next.js environment. You should pre careted ``createServer()`` and ``fetch()`` functions they are available via just ``require`` (``import``).
105 |
106 | ```js
107 | const next = require("next");
108 | const http = require("http");
109 | const express = require("express");
110 | const bodyParser = require("body-parser");
111 | const createServer = require("shadow-fetch");
112 | const { shadowFetchMiddleware } = require("shadow-fetch-express");
113 |
114 | const dev = process.env.NODE_ENV !== "production";
115 | const app = next({ dev });
116 | const handle = app.getRequestHandler();
117 |
118 | app.prepare().then(() => {
119 | const server = express();
120 | server.use(shadowFetchMiddleware);
121 | server.use(bodyParser.json());
122 | server.get("/api/message", (req, res) => {
123 | res.json({message: "hello via shadow-fetch"});
124 | });
125 | server.get("*", (req, res) => {
126 | return handle(req, res);
127 | });
128 | // enable shadow fetch entrypoint
129 | createServer(server).listen();
130 | // enable standard HTTP entrypoint
131 | http.createServer(server).listen(3000, err => {
132 | if (err) throw err;
133 | console.log("> Ready on http://localhost:3000");
134 | });
135 | });
136 | ```
137 |
138 | ## Server Side Rendering and Authentication
139 |
140 | Sometimes, you want to add authentication feature. The standard ``fetch`` were called from browseres and shadow-fetch was called during server side rendering.
141 |
142 | You can detect the client environment inside event handler. If the API checks authentication, you should check ``shadow`` property.
143 |
144 | ```js
145 | const isAuthenticatedAPI = (req, res, next) => {
146 | if (req.shadow || req.isAuthenticated()) {
147 | return next();
148 | } else {
149 | res.sendStatus(403);
150 | }
151 | };
152 |
153 | server.get("/api/item/:id", isAuthenticatedAPI, (req, res) => {
154 | // API implementation
155 | });
156 | ```
157 |
158 | [This is why I started to make this package](https://github.com/zeit/next.js/issues/3797).
159 |
160 | ## Client API
161 |
162 | ```js
163 | const { fetch, Headers } = require("shadow-fetch");
164 | ```
165 |
166 | It provides three functions that are almost compatible with standard [``fetch()``](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API):
167 |
168 | * ``shadowFetch()``: Provides direct access between ``createServer``.
169 | * ``fetch()``: Alias of ``shadowFetch()`` or standard ``fetch()``.
170 |
171 | Usually ``fetch()`` is the only function you use.
172 |
173 | | Name | on Node.js | on Browser |
174 | |:-----------:|:-----------|:------------|
175 | | ``fetch`` | ``shadowFetch`` | standard ``fetch`` |
176 | | ``shadowFetch`` | ``shadowFetch`` | ``shadowFetch`` |
177 |
178 | If you want to select actual HTTP access or not explicitly, use regular ``fetch()``.
179 |
180 | This library provides ``Headers`` compatible class too. But there is no ``Request`` class now.
181 |
182 | ## Server API
183 |
184 | * ``createServer()``: It is a compatible function of Node.js's ``http.createServer()``. This package provides ``createShadowServer()`` for your convenience.
185 | * ``IncomingMessage``: Request object of server code. It is also compatible with Node.js's ``IncomingMessage`` except the following members:
186 |
187 | * ``shadow`` property: It always ``true``. You can identify the request is made by ``shadowFetch`` or regular HTTP access.
188 |
189 | * ``ServerResponse``: Response object of server code. It is also compatible with Node.js's ``ServerResponse`` except the following members:
190 |
191 | * ``writeJSON()``: It stores JSON without calling ``JSON.stringify()`` function. You can get JSON directly via ``Response#json()`` method of ``shadowFetch()``.
192 |
193 | ```js
194 | const { fetch, createServer } = require("shadow-fetch");
195 | ```
196 |
197 | ## Utility Function
198 |
199 | * ``initFetch()``: It generates ``fetch()`` (shadow version) and ``createServer()`` they are connected internally. It is good for writing unit tests.
200 |
201 | ## Trouble Shooting
202 |
203 | ### ``"shadow-fetch is not initialized properly. See https://github.com/shibukawa/shadow-fetch#trouble-shooting."``
204 |
205 | This error is thrown when ``fetch`` is used without initialization. You should call shadow-fetch's ``createServer`` like [this](https://github.com/shibukawa/shadow-fetch#nodejss-http-server).
206 |
207 | ### Error occures inside web server handlers
208 |
209 | Express-middleware is not installed. Read [this section](https://github.com/shibukawa/shadow-fetch#expressjs).
210 |
211 | Express.js overwrite prototype of ``ServerResponse``/``IncomingMessage`` with http's ones inside its framework.
212 | In that case, required properties are not initilized because original constructor is not called.
213 | So some method calls are failed like the following error:
214 |
215 | ```
216 | TypeError: Cannot read property 'push' of undefined
217 | at ServerResponse._writeRaw (_http_outgoing.js:281:24)
218 | at ServerResponse._send (_http_outgoing.js:240:15)
219 | at ServerResponse.end (_http_outgoing.js:770:16)
220 | at ServerResponse.end (/Users/shibukawa/develop/frx/dam-front/node_modules/compression/index.js:107:21)
221 | at ServerResponse.send (/Users/shibukawa/develop/frx/dam-front/node_modules/express/lib/response.js:221:10)
222 | ```
223 |
224 | ### ``Error: bundles/pages/index.js from UglifyJs Name expected`` during ``next build``
225 |
226 | Your next.js version is low. Try 5.1.0.
227 |
228 | ## License
229 |
230 | MIT
231 |
--------------------------------------------------------------------------------
/browser.js:
--------------------------------------------------------------------------------
1 | const fetch = window.fetch || (window.fetch = require("unfetch").default || require("unfetch"));
2 |
3 | const {
4 | initFetch,
5 | shadowFetch,
6 | createServer,
7 | Headers,
8 | IncomingMessage,
9 | ServerResponse
10 | } = require("./index");
11 |
12 | module.exports = {
13 | initFetch,
14 | fetch,
15 | shadowFetch,
16 | createServer,
17 | Headers,
18 | IncomingMessage,
19 | ServerResponse
20 | };
21 |
--------------------------------------------------------------------------------
/doc/shadow-fetch.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/shibukawa/shadow-fetch/8fe71327d9530d3dc19bf4d2be4adf8a087826f9/doc/shadow-fetch.png
--------------------------------------------------------------------------------
/examples/next.js/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-js-example",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "scripts": {
7 | "build": "next build",
8 | "dev": "node server.js",
9 | "start": "cross-env NODE_ENV=production node server.js"
10 | },
11 | "keywords": [],
12 | "author": "",
13 | "license": "MIT",
14 | "dependencies": {
15 | "cross-env": "^5.1.4",
16 | "express": "^4.16.2",
17 | "next": "^5.0.0",
18 | "prop-types": "^15.6.1",
19 | "react": "^16.2.0",
20 | "react-dom": "^16.2.0",
21 | "shadow-fetch": "file:shadow-fetch-0.1.2.tgz",
22 | "shadow-fetch-express": "file:shadow-fetch-express-0.1.2.tgz"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/next.js/pages/index.js:
--------------------------------------------------------------------------------
1 | import React, {Component} from "react";
2 | import PropTypes from "prop-types";
3 | import { fetch } from "../../../";
4 | //import { fetch } from "shadow-fetch";
5 |
6 | export default class Index extends Component {
7 | static async getInitialProps({req}) {
8 | const res = await fetch("/api/message");
9 | const message = await res.json();
10 | return message;
11 | }
12 |
13 | render() {
14 | return
15 | Message from shadow-fetch:
16 | {this.props.message}
17 |
;
18 | }
19 | }
20 |
21 | Index.propTypes = {
22 | message: PropTypes.string
23 | };
24 |
--------------------------------------------------------------------------------
/examples/next.js/server.js:
--------------------------------------------------------------------------------
1 | const next = require("next");
2 | const { createServer } = require("http");
3 | const express = require("express");
4 | const bodyParser = require("body-parser");
5 | const { createShadowServer } = require("shadow-fetch");
6 | const { shadowFetchMiddleware } = require("shadow-fetch-express");
7 |
8 |
9 | const dev = process.env.NODE_ENV !== "production";
10 | const app = next({ dev });
11 | const handle = app.getRequestHandler();
12 |
13 |
14 | app.prepare().then(() => {
15 | const server = express();
16 | server.use(shadowFetchMiddleware);
17 | server.use(bodyParser.json());
18 | server.get("/api/message", (req, res) => {
19 | res.json({message: "hello via shadow-fetch"});
20 | });
21 | server.get("*", (req, res) => {
22 | return handle(req, res);
23 | });
24 | // enable shadow fetch entrypoint
25 | createShadowServer(server).listen();
26 | // enable standard HTTP entrypoint
27 | createServer(server).listen(3000, err => {
28 | if (err) throw err;
29 | console.log("> Ready on http://localhost:3000");
30 | });
31 | });
32 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | const { Headers } = require("./lib/headers");
2 | const { ServerResponse } = require("./lib/serverresponse");
3 | const { IncomingMessage, shadowKey } = require("./lib/incomingmessage");
4 | const { Response } = require("./lib/response");
5 |
6 |
7 | class Connection {
8 | constructor() {
9 | this._handler = null;
10 | this.fetch = this.fetch.bind(this);
11 | }
12 |
13 | async fetch(url, opts) {
14 | return new Promise((resolve, reject) => {
15 | if (!this._handler) {
16 | return reject(new Error("shadow-fetch is not initialized properly. See https://github.com/shibukawa/shadow-fetch#trouble-shooting."))
17 | }
18 | let redirected = false;
19 | const receiveResponse = (res) => {
20 | if ([301, 302, 303, 307, 308].includes(res.statusCode)) {
21 | redirected = true;
22 | this._handler(
23 | new IncomingMessage(res._headers.get("location"), opts),
24 | new ServerResponse(receiveResponse));
25 | } else {
26 | const clientResponse = new Response(res);
27 | clientResponse._redirected = redirected;
28 | resolve(clientResponse);
29 | }
30 | };
31 | this._handler(new IncomingMessage(url, opts), new ServerResponse(receiveResponse));
32 | });
33 | }
34 | }
35 |
36 |
37 | class Server {
38 | constructor(connection, handler) {
39 | connection._handler = handler;
40 | this._address = {
41 | port: Math.floor(Math.random() * (65536 - 1024)) + 1024,
42 | address: "127.0.0.1",
43 | family: "IPv4",
44 | shadow: true
45 | };
46 | this._listening = false;
47 | }
48 |
49 | get listening() {
50 | return this._listening;
51 | }
52 |
53 | listen(...args) {
54 | if (typeof args[0] === "number") {
55 | this._address.port = args[0];
56 | args.splice(0, 1);
57 | }
58 | if (typeof args[0] === "function") {
59 | process.nextTick(args[0]);
60 | }
61 | this._listening = true;
62 | }
63 |
64 | address() {
65 | return this._address;
66 | }
67 |
68 | close(callback) {
69 | this._listening = false;
70 | if (callback) {
71 | process.nextTick(callback);
72 | }
73 | }
74 | }
75 |
76 | const initFetch = () => {
77 | const connection = new Connection();
78 | return {
79 | fetch(url, opts) {
80 | return connection.fetch(url, opts);
81 | },
82 | createServer(handler) {
83 | return new Server(connection, handler);
84 | }
85 | };
86 | };
87 |
88 | if (typeof window === "undefined") {
89 | if (!global.shadowFetch) {
90 | const { fetch, createServer } = initFetch();
91 | global.shadowFetch = fetch;
92 | global.createShadowServer = createServer;
93 | }
94 | module.exports = global.shadowFetch;
95 | module.exports.shadowFetch = module.exports.fetch = global.shadowFetch;
96 | module.exports.createShadowServer = module.exports.createServer = global.createShadowServer;
97 | }
98 |
99 | module.exports.initFetch = initFetch;
100 | module.exports.Headers = Headers;
101 | module.exports.IncomingMessage = IncomingMessage;
102 | module.exports.ServerResponse = ServerResponse;
103 | module.exports.shadowKey = shadowKey;
104 |
--------------------------------------------------------------------------------
/lib/headers.js:
--------------------------------------------------------------------------------
1 | // this code is based on https://github.com/github/fetch/blob/master/fetch.js
2 |
3 | function normalizeName(name) {
4 | if (typeof name !== "string") {
5 | name = String(name);
6 | }
7 | if (/[^a-z0-9\-#$%&'*+.^_`|~]/i.test(name)) {
8 | throw new TypeError("Invalid character in header field name");
9 | }
10 | return name.toLowerCase();
11 | }
12 |
13 | function normalizeValue(value) {
14 | if (typeof value !== "string") {
15 | value = String(value);
16 | }
17 | return value;
18 | }
19 |
20 |
21 | class Headers {
22 | constructor(headers) {
23 | this._map = new Map();
24 | this._clientMode = true;
25 | if (headers instanceof Headers) {
26 | for (const [name, value] of headers.entries()) {
27 | this._map.set(name, value);
28 | }
29 | } else if (Array.isArray(headers)) {
30 | for (const [name, value] of headers) {
31 | this._map.set(name, value);
32 | }
33 | } else if (headers) {
34 | for (const [name, value] of Object.entries(headers)) {
35 | this._map.set(normalizeName(name), normalizeValue(value));
36 | }
37 | }
38 | }
39 |
40 | append(name, value) {
41 | name = normalizeName(name);
42 | value = normalizeValue(value);
43 | var oldValue = this._map.get(name);
44 | this._map.set(name, oldValue ? oldValue + ", " + value : value);
45 | }
46 |
47 | delete(name) {
48 | return this._map.delete(normalizeName(name));
49 | }
50 |
51 | get(name) {
52 | name = normalizeName(name);
53 | if (this._map.has(name)) {
54 | return this._map.get(name);
55 | }
56 | return null;
57 | }
58 |
59 | has(name) {
60 | return this._map.has(normalizeName(name));
61 | }
62 |
63 | set(name, value) {
64 | if (this._clientMode) {
65 | this._map.set(normalizeName(name), normalizeValue(value));
66 | } else {
67 | this._map.set(normalizeName(name), value);
68 | }
69 | return this;
70 | }
71 |
72 | forEach(callback, thisArg) {
73 | this._map.forEach(callback, thisArg);
74 | }
75 |
76 | keys() {
77 | return this._map.keys();
78 | }
79 |
80 | values() {
81 | return this._map.values();
82 | }
83 |
84 | entries() {
85 | return this._map.entries();
86 | }
87 | }
88 |
89 | const convertToClientMode = (headers) => {
90 | for (const [key, value] of headers.entries()) {
91 | if (Array.isArray(value)) {
92 | headers._map.set(key, value.join(", "));
93 | }
94 | }
95 | };
96 |
97 | const convertToServerHeaders = (headers) => {
98 | const serverHeaders = {};
99 | const serverRawHeaders = [];
100 | for (const [name, value] of headers.entries()) {
101 | serverHeaders[name] = value;
102 | serverRawHeaders.push(name, value);
103 | }
104 |
105 | return {
106 | headers: serverHeaders,
107 | rawHeaders: serverRawHeaders
108 | };
109 | };
110 |
111 | module.exports = {
112 | Headers,
113 | convertToClientMode,
114 | convertToServerHeaders
115 | };
116 |
--------------------------------------------------------------------------------
/lib/incomingmessage.js:
--------------------------------------------------------------------------------
1 | const { Headers, convertToServerHeaders } = require("./headers");
2 |
3 | // HTTP methods whose capitalization should be normalized
4 | var methods = ["DELETE", "GET", "HEAD", "OPTIONS", "POST", "PUT"];
5 |
6 | function normalizeMethod(method) {
7 | var upcased = method.toUpperCase();
8 | return (methods.indexOf(upcased) > -1) ? upcased : method;
9 | }
10 |
11 | const bodyKey = Symbol("body");
12 | const textKey = Symbol("text");
13 | const jsonKey = Symbol("json");
14 | const formKey = Symbol("form");
15 | const shadowKey = Symbol("shadow");
16 |
17 | class IncomingMessage {
18 | constructor(url, { method, headers, body } = {}) {
19 | this[shadowKey] = true;
20 | if (!url) {
21 | return;
22 | }
23 | this.url = url;
24 | this.method = method ? normalizeMethod(method) : "GET";
25 | this.httpVersion = "1.1";
26 | this.ip = "::1"
27 | const hasHeaders = !!headers;
28 | if (headers && !(headers instanceof Headers)) {
29 | headers = new Headers(headers);
30 | }
31 |
32 | let contentType;
33 | let contentLength = 0;
34 | if (body) {
35 | if (!Buffer.isBuffer(body)) {
36 | if (typeof body === "string") {
37 | this[textKey] = body;
38 | body = new Buffer(body, "utf8");
39 | } else {
40 | body = new Buffer(body.toString(), "utf8");
41 | }
42 | }
43 | this[bodyKey] = body;
44 | contentType = "text/plain";
45 | contentLength = String(body.length);
46 | }
47 |
48 | if (hasHeaders) {
49 | if (contentType) {
50 | if (!headers.has("content-type")) {
51 | headers.set("content-type", contentType);
52 | }
53 | headers.set("content-length", contentLength);
54 | }
55 | const serverHeader = convertToServerHeaders(headers);
56 | this.headers = serverHeader.headers;
57 | this.rawHeaders = serverHeader.rawHeaders;
58 | } else if (contentType) {
59 | this.headers = {
60 | "content-type": contentType,
61 | "content-length": contentLength
62 | };
63 | this.rawHeaders = [
64 | "content-type", contentType,
65 | "content-length", contentLength
66 | ];
67 | } else {
68 | this.headers = {};
69 | this.rawHeaders = [];
70 | }
71 | this.trailers = {};
72 | this.rawTrailers = [];
73 |
74 | this._sendBody = false;
75 | this._listeners = new Map();
76 | }
77 |
78 | get shadow() {
79 | return !!this[shadowKey];
80 | }
81 |
82 | on(eventType, handler) {
83 | const listneers = this._listeners.get(eventType);
84 | if (listneers) {
85 | listneers.push(handler);
86 | } else {
87 | this._listeners.set(eventType, [handler]);
88 | }
89 | if (eventType === "data") {
90 | handler(this[bodyKey]);
91 | this._sendBody = true;
92 | const listeners = this._listeners.get("end");
93 | if (listeners) {
94 | for (const listener of listeners) {
95 | listener();
96 | }
97 | }
98 | } else if (eventType === "end") {
99 | if (this._sendBody) {
100 | handler();
101 | } else {
102 | this._endListener = handler;
103 | }
104 | }
105 | }
106 |
107 | listeners(eventName) {
108 | return this._listeners.get(eventName) || [];
109 | }
110 |
111 | resume() {
112 | }
113 |
114 | pipe(dest) {
115 | dest.emit("pipe", this);
116 | this.on("end", () => {
117 | dest.end();
118 | });
119 | this.on("data", (data) => {
120 | dest.write(data);
121 | });
122 | return dest;
123 | }
124 |
125 | removeListener(event, handler) {
126 | const listeners = this._listeners.get(event);
127 | listeners.splice(listeners.indexOf(handler), 1);
128 | }
129 | }
130 |
131 | module.exports = {
132 | IncomingMessage,
133 | bodyKey,
134 | jsonKey,
135 | formKey,
136 | textKey,
137 | shadowKey
138 | };
139 |
--------------------------------------------------------------------------------
/lib/response.js:
--------------------------------------------------------------------------------
1 | const { statusTexts } = require("./statustexts");
2 | const { jsonKey } = require("./incomingmessage");
3 |
4 | class Response {
5 | constructor(serverResponse) {
6 | this._response = serverResponse;
7 | this._redirected = false;
8 | }
9 |
10 | get status() {
11 | return this._response.statusCode;
12 | }
13 |
14 | get ok() {
15 | return this._response.statusCode < 299;
16 | }
17 |
18 | get statusText() {
19 | if (this._response.statusMessage) {
20 | return this._response.statusMessage;
21 | } else {
22 | return statusTexts.get(this._response.statusCode);
23 | }
24 | }
25 |
26 | get headers() {
27 | return this._response._headers;
28 | }
29 |
30 | get redirected() {
31 | return this._redirected;
32 | }
33 |
34 | get type() {
35 | return "shadow";
36 | }
37 |
38 | get url() {
39 | return "/";
40 | }
41 |
42 | async arraybuffer() {
43 | throw new Error("not implemented");
44 | }
45 |
46 | async blob() {
47 | throw new Error("not implemented");
48 | }
49 |
50 | async json() {
51 | const json = this._response[jsonKey];
52 | if (json) {
53 | return json;
54 | }
55 | const text = await this.text();
56 | return JSON.parse(text);
57 | }
58 |
59 | async text() {
60 | const outputs = this._response.output;
61 | if (outputs.length === 0) {
62 | return "";
63 | } else if (outputs.length === 1) {
64 | if (typeof outputs[0] === "string") {
65 | return outputs[0];
66 | } else if (Buffer.isBuffer(outputs[0])) {
67 | return outputs[0].toString("utf8");
68 | } else {
69 | throw new Error("unsupported type: ", outputs[0]);
70 | }
71 | }
72 | let result = "";
73 | for (const output of outputs) {
74 | if (typeof output === "string") {
75 | result = result + output;
76 | } else if (Buffer.isBuffer(output)) {
77 | result = result + output.toString("utf8");
78 | } else {
79 | throw new Error("unsupported type: ", output);
80 | }
81 | }
82 | return result;
83 | }
84 | }
85 |
86 | module.exports = {
87 | Response
88 | };
89 |
--------------------------------------------------------------------------------
/lib/serverresponse.js:
--------------------------------------------------------------------------------
1 | const { Headers } = require("./headers");
2 | const { jsonKey } = require("./incomingmessage");
3 |
4 |
5 | class ServerResponse {
6 | constructor(onfinish) {
7 | this.statusCode = 200;
8 | this.statusMessage = "";
9 | this._headers = new Headers();
10 | this._trailers = null;
11 | this.output = [];
12 | this._onfinish = onfinish;
13 | this.sendDate = false;
14 | this.headersSent = false;
15 | this._readableStream = null;
16 | this._drainListener = null;
17 | this._listeners = {
18 | close: null,
19 | drain: null,
20 | error: null,
21 | finish: null,
22 | pipe: null,
23 | unpipe: null
24 | };
25 | this._readableStream;
26 | }
27 |
28 | get shadow() {
29 | return true;
30 | }
31 |
32 | on(eventName, listener) {
33 | if (!this._listeners.hasOwnProperty(eventName)) {
34 | throw new Error(`Unsupported event: '${eventName}'`);
35 | }
36 | this._listeners[eventName] = { listener, once: false };
37 | }
38 |
39 | once(eventName, listener) {
40 | if (!this._listeners.hasOwnProperty(eventName)) {
41 | throw new Error(`Unsupported event: '${eventName}'`);
42 | }
43 | this._listeners[eventName] = { listener, once: true };
44 | }
45 |
46 | emit(eventName, arg) {
47 | if (!this._listeners.hasOwnProperty(eventName)) {
48 | throw new Error(`Unsupported event: '${eventName}'`);
49 | }
50 | this._emit(eventName, arg);
51 | if (eventName === "pipe") {
52 | if (!this._listeners.unpipe) {
53 | // Stream v1
54 | arg.on("data", (arg) => {
55 | this.write(arg);
56 | });
57 | }
58 | this._readableStream = arg;
59 | this._emit("drain", arg);
60 | }
61 | }
62 |
63 | _emit(eventName, arg) {
64 | if (this._listeners[eventName]) {
65 | const { listener, once } = this._listeners[eventName];
66 | listener(arg);
67 | if (once) {
68 | delete this._listeners[eventName];
69 | }
70 | }
71 | }
72 |
73 | removeListener(eventName, listener) {
74 | if (!this._listeners.hasOwnProperty(eventName)) {
75 | throw new Error(`Unsupported event: '${eventName}'`);
76 | }
77 | if (this._listeners[eventName] && this._listeners[eventName].listener === listener) {
78 | delete this._listeners[eventName];
79 | }
80 | }
81 |
82 | pipe() {
83 | throw new Error("Can't call ServerResponse's pipe because it is not readable");
84 | }
85 |
86 | addTrailers(headers) {
87 | this._trailers = headers;
88 | }
89 |
90 | getHeader(name) {
91 | return this._headers.get(name);
92 | }
93 |
94 | getHeaderNames() {
95 | return Array.from(this._headers.keys());
96 | }
97 |
98 | getHeaders() {
99 | return this._headers.entries();
100 | }
101 |
102 | hasHeader(name) {
103 | return this._headers.has(name);
104 | }
105 |
106 | removeHeader(name) {
107 | if (this.headersSent) {
108 | throw new Error("[ERR_HTTP_HEADERS_SENT]: Cannot remove headers after they are sent to the client");
109 | } else {
110 | this._headers.delete(name);
111 | }
112 | }
113 |
114 | setHeader(name, value) {
115 | if (this.headersSent) {
116 | throw new Error("[ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client");
117 | } else {
118 | this._headers.set(name, value);
119 | }
120 | }
121 |
122 | listeners() {
123 | return [];
124 | }
125 |
126 | writeHead(statusCode, headers) {
127 | this.statusCode = statusCode;
128 | if (headers) {
129 | for (const [name, value] of Object.entries(headers)) {
130 | this._headers.set(name, value);
131 | }
132 | }
133 | }
134 |
135 | write(chunk, encoding, callback) {
136 | if (typeof chunk === "string" && typeof encoding === "string") {
137 | this.output.push(Buffer.from(chunk, encoding));
138 | } else if (chunk) {
139 | this.output.push(chunk);
140 | }
141 | if (callback) {
142 | throw new Error("not implemented: callback of write()");
143 | }
144 | return true;
145 | }
146 |
147 | end(chunk, encoding, callback) {
148 | if (typeof chunk === "string" && typeof encoding === "string") {
149 | this.output.push(Buffer.from(chunk, encoding));
150 | } else if (chunk) {
151 | this.output.push(chunk);
152 | }
153 | if (callback) {
154 | throw new Error("not implemented: callback of write()");
155 | }
156 | this._onfinish(this);
157 | }
158 |
159 | writeJSON(json) {
160 | this[jsonKey] = json;
161 | }
162 | }
163 |
164 | module.exports = {
165 | ServerResponse
166 | };
167 |
--------------------------------------------------------------------------------
/lib/statustexts.js:
--------------------------------------------------------------------------------
1 | const statusTexts = new Map([
2 | [100, "Continue"],
3 | [101, "Switching Protocols"],
4 | [200, "OK"],
5 | [201, "Created"],
6 | [202, "Accepted"],
7 | [203, "Non - Authoritative Information"],
8 | [204, "No Content"],
9 | [205, "Reset Content"],
10 | [206, "Partial Content"],
11 | [300, "Multiple Choices"],
12 | [301, "Moved Permanently"],
13 | [302, "Found"],
14 | [303, "See Other"],
15 | [304, "Not Modified"],
16 | [307, "Temporary Redirect"],
17 | [308, "Permanent Redirect"],
18 | [400, "Bad Request"],
19 | [401, "Unauthorized"],
20 | [403, "Forbidden"],
21 | [404, "Not Found"],
22 | [405, "Method Not Allowed"],
23 | [406, "Not Acceptable"],
24 | [407, "Proxy Authentication Required"],
25 | [408, "Request Timeout"],
26 | [409, "Conflict"],
27 | [410, "Gone"],
28 | [411, "Length Required"],
29 | [412, "Precondition Failed"],
30 | [413, "Payload Too Large"],
31 | [414, "URI Too Long"],
32 | [415, "Unsupported Media Type"],
33 | [416, "Range Not Satisfiable"],
34 | [417, "Expectation Failed"],
35 | [418, "I'm a teapot"],
36 | [426, "Upgrade Required"],
37 | [428, "Precondition Required"],
38 | [429, "Too Many Requests"],
39 | [431, "Request Header Fields Too Large"],
40 | [451, "Unavailable For Legal Reasons"],
41 | [500, "Internal Server Error"],
42 | [501, "Not Implemented"],
43 | [502, "Bad Gateway"],
44 | [503, "Service Unavailable"],
45 | [504, "Gateway Timeout"],
46 | [505, "HTTP Version Not Supported"],
47 | [511, "Network Authentication Required"]]);
48 | module.exports = {
49 | statusTexts
50 | };
51 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shadow-fetch",
3 | "version": "0.2.1",
4 | "description": "fake fetch/httpServer for SSR",
5 | "main": "index.js",
6 | "browser": "browser.js",
7 | "scripts": {
8 | "test": "nyc ava -v",
9 | "benchmark": "ava -v --serial",
10 | "coverage": "nyc report --reporter=text-lcov > coverage.lcov && codecov",
11 | "security-test": "snyk test"
12 | },
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/shibukawa/shadow-fetch"
16 | },
17 | "keywords": [
18 | "isomorphic",
19 | "fetch",
20 | "next.js",
21 | "express.js"
22 | ],
23 | "author": "Yoshiki Shibukawa",
24 | "license": "MIT",
25 | "devDependencies": {
26 | "ava": "^0.25.0",
27 | "browserify": "^16.2.0",
28 | "codecov": "^3.0.0",
29 | "eslint": "^4.19.1",
30 | "express": "^4.16.3",
31 | "flow-bin": "^0.70.0",
32 | "node-fetch": "^2.1.2",
33 | "nyc": "^11.6.0",
34 | "override-require": "^1.1.1",
35 | "snyk": "^1.78.1",
36 | "streamtest": "^1.2.3",
37 | "unfetch": "^3.0.0"
38 | },
39 | "ava": {
40 | "files": [
41 | "test/test-*.js"
42 | ],
43 | "source": [
44 | "lib/**/*.{js,jsx}",
45 | "index.js"
46 | ]
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/shadow-fetch-express/.npmignore:
--------------------------------------------------------------------------------
1 | doc/*.png
2 | test
3 | examples
4 | node_modules
5 | shadow-fetch-express
6 | .nyc_output
7 | .vscode
8 | coverage
9 | *.tgz
10 |
--------------------------------------------------------------------------------
/shadow-fetch-express/LICENSE.md:
--------------------------------------------------------------------------------
1 | ## MIT License
2 |
3 | Copyright © 2018 Yoshiki Shibukawa
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a
6 | copy of this software and associated documentation files (the "Software"),
7 | to deal in the Software without restriction, including without limitation
8 | the rights to use, copy, modify, merge, publish, distribute, sublicense,
9 | and/or sell copies of the Software, and to permit persons to whom the
10 | Software is furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21 | DEALINGS IN THE SOFTWARE.
22 |
--------------------------------------------------------------------------------
/shadow-fetch-express/README.md:
--------------------------------------------------------------------------------
1 | # shadow-dom-express middleware
2 |
3 | [](https://badge.fury.io/js/shadow-fetch-express)
4 | [](https://nodei.co/npm/shadow-fetch-express/)
5 |
6 | see: https://github.com/shibukawa/shadow-fetch
7 |
--------------------------------------------------------------------------------
/shadow-fetch-express/index.js:
--------------------------------------------------------------------------------
1 | const { IncomingMessage, shadowKey, ServerResponse } = require("shadow-fetch");
2 |
3 | const shadowFetchMiddleware = (req, res, next) => {
4 | if (req[shadowKey]) {
5 | for (const key of Object.getOwnPropertyNames(IncomingMessage.prototype)) {
6 | if (key !== "constructor" || key !== "shadow") {
7 | req[key] = IncomingMessage.prototype[key];
8 | }
9 | }
10 | Object.defineProperty(req, "shadow", Object.getOwnPropertyDescriptor(IncomingMessage.prototype, "shadow"));
11 | for (const key of Object.getOwnPropertyNames(ServerResponse.prototype)) {
12 | if (key !== "constructor") {
13 | res[key] = ServerResponse.prototype[key];
14 | }
15 | }
16 |
17 | res.json = function (obj) {
18 | let val = obj;
19 | // allow status / body
20 | if (arguments.length === 2) {
21 | // res.json(body, status) backwards compat
22 | if (typeof arguments[1] === "number") {
23 | this.statusCode = arguments[1];
24 | } else {
25 | this.statusCode = arguments[0];
26 | val = arguments[1];
27 | }
28 | }
29 | // content-type
30 | if (!this.get("Content-Type")) {
31 | this.set("Content-Type", "application/json");
32 | }
33 | this.writeJSON(val);
34 | return this.end();
35 | };
36 | }
37 | next();
38 | };
39 |
40 | module.exports = {
41 | shadowFetchMiddleware
42 | };
43 |
--------------------------------------------------------------------------------
/shadow-fetch-express/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "shadow-fetch-express",
3 | "version": "0.2.0",
4 | "description": "express.js middleware of shadow-fetch",
5 | "main": "index.js",
6 | "scripts": {
7 | "test": "echo \"Error: no test specified\" && exit 1"
8 | },
9 | "repository": {
10 | "type": "git",
11 | "url": "https://github.com/shibukawa/shadow-fetch"
12 | },
13 | "keywords": [
14 | "isomorphic",
15 | "fetch",
16 | "next.js",
17 | "express.js"
18 | ],
19 | "dependencies": {
20 | "shadow-fetch": "^0.2.0"
21 | },
22 | "author": "Yoshiki Shibukawa",
23 | "license": "MIT"
24 | }
25 |
--------------------------------------------------------------------------------
/test/helper.js:
--------------------------------------------------------------------------------
1 | const fetch = require("node-fetch");
2 | const { createServer } = require("http");
3 |
4 | module.exports = {
5 | initStandardFetch() {
6 | return {
7 | fetch,
8 | createServer
9 | };
10 | },
11 | printBenchmark(message, [sec, nanosec]) {
12 | let microsec = Math.round(nanosec / 1000).toString(10);
13 | for (let i = microsec.length; i < 6; i++) {
14 | microsec = "0" + microsec;
15 | }
16 | return `${message}: ${sec}.${microsec} seconds`;
17 | }
18 | };
19 |
--------------------------------------------------------------------------------
/test/test-benchmark.js:
--------------------------------------------------------------------------------
1 | const test = require("ava");
2 | const { initStandardFetch, printBenchmark } = require("./helper");
3 |
4 | test("the actual connection and receive json", async (t) => {
5 | const { fetch, createServer } = initStandardFetch();
6 |
7 | const server = createServer((req, res) => {
8 | res.writeHead(200, { "Content-Type": "application/json" });
9 | res.end(JSON.stringify({ message: "hello" }));
10 | });
11 |
12 | return new Promise((resolve) => {
13 | server.listen(async () => {
14 | const { port } = server.address();
15 |
16 | const start = process.hrtime();
17 | const res = await fetch(`http://localhost:${port}/test`);
18 | const json = await res.json();
19 | const end = process.hrtime(start);
20 |
21 | t.is(res.status, 200);
22 | t.is(res.statusText, "OK");
23 | t.is(res.ok, true);
24 |
25 | t.is(json.message, "hello");
26 | t.log(printBenchmark("Benchmark regular fetch and server", end));
27 | server.close(resolve);
28 | });
29 | });
30 | });
31 | /*
32 | test("write chunks and receive text", async (t) => {
33 | const { fetch, createServer } = initFetch();
34 |
35 | const server = createServer((req, res) => {
36 | res.writeHead(200, { "Content-Type": "application/json" });
37 | res.write("hello\n");
38 | res.write("world\n");
39 | res.end();
40 | });
41 |
42 | server.listen(80);
43 |
44 | const res = await fetch("/test");
45 | t.is(res.status, 200);
46 | t.is(res.statusText, "OK");
47 | t.is(res.ok, true);
48 |
49 | const message = await res.text();
50 | t.is(message, "hello\nworld\n");
51 | });
52 |
53 | test("POST method and receive header", async (t) => {
54 | const { fetch, createServer } = initFetch();
55 |
56 | const server = createServer((req, res) => {
57 | t.is(req.method, "POST");
58 | res.writeHead(201, { "Location": "/test/2" });
59 | res.end();
60 | });
61 |
62 | server.listen(80);
63 |
64 | const res = await fetch("/test", { method: "POST" });
65 | t.is(res.status, 201);
66 | t.is(res.statusText, "Created");
67 | t.is(res.ok, true);
68 | t.is(res.headers.get("location", "/test/2"));
69 | });*/
70 |
--------------------------------------------------------------------------------
/test/test-express.js:
--------------------------------------------------------------------------------
1 | const test = require("ava");
2 | const express = require("express");
3 | const bodyParser = require("body-parser");
4 | const restoreOriginalRequire = require("override-require");
5 |
6 |
7 | // override require to resolve shadow-fetch form subproject
8 | const isOverride = (request) => {
9 | return request === "shadow-fetch";
10 | };
11 | const resolveRequest = () => {
12 | return require("../index");
13 | };
14 | restoreOriginalRequire(isOverride, resolveRequest);
15 |
16 |
17 | const { initFetch } = require("../index");
18 | const { shadowFetchMiddleware } = require("../shadow-fetch-express/index");
19 |
20 | test("use express middleware", async (t) => {
21 | const app = express();
22 | app.use(shadowFetchMiddleware);
23 | app.use(bodyParser.json());
24 | app.post("/test", (req, res) => {
25 | t.is(req.ip, "::1");
26 | t.is(req.shadow, true);
27 | t.is(req.body.message, "hello");
28 | res.send({ message: "world" });
29 | });
30 | const { fetch, createServer } = initFetch();
31 |
32 | createServer(app);
33 |
34 | const res = await fetch("/test", {
35 | method: "post",
36 | headers: {
37 | "content-type": "application/json"
38 | },
39 | body: JSON.stringify({ message: "hello" })
40 | });
41 | t.is(res.status, 200);
42 | t.is(res.statusText, "OK");
43 | t.is(res.ok, true);
44 | const json = await res.json();
45 | t.is(json.message, "world");
46 | });
47 |
48 | test("use middleware and shadow fetch's json() method", async (t) => {
49 | const app = express();
50 | app.use(shadowFetchMiddleware);
51 | app.get("/test", (req, res) => {
52 | t.is(req.shadow, true);
53 | res.json({ message: "world" });
54 | });
55 | const { fetch, createServer } = initFetch();
56 |
57 | createServer(app);
58 | const res = await fetch("/test");
59 | t.is(res.status, 200);
60 | t.is(res.statusText, "OK");
61 | t.is(res.ok, true);
62 | const json = await res.json();
63 | t.is(json.message, "world");
64 | });
65 |
--------------------------------------------------------------------------------
/test/test-headers.js:
--------------------------------------------------------------------------------
1 | const test = require("ava");
2 | const {
3 | Headers,
4 | convertToClientMode,
5 | convertToServerHeaders
6 | } = require("../lib/headers");
7 |
8 | test("empty headers", (t) => {
9 | const headers = new Headers();
10 |
11 | t.deepEqual(Array.from(headers.keys()), []);
12 | t.deepEqual(Array.from(headers.values()), []);
13 | t.deepEqual(Array.from(headers.entries()), []);
14 |
15 | t.is(headers.get("no-exist-key"), null);
16 | t.is(headers.has("no-exist-key"), false);
17 | });
18 |
19 | test("set/get", (t) => {
20 | const headers = new Headers();
21 |
22 | headers.set("accept-encoding", "gzip");
23 |
24 | t.is(headers.get("accept-encoding"), "gzip");
25 | t.is(headers.has("accept-encoding"), true);
26 |
27 | t.is(headers.get("Accept-Encoding"), "gzip");
28 | t.is(headers.has("Accept-Encoding"), true);
29 |
30 | headers.append("Content-Type", "application/json");
31 |
32 | t.is(headers.get("content-type"), "application/json");
33 | t.is(headers.has("content-type"), true);
34 |
35 | t.is(headers.get("Content-Type"), "application/json");
36 | t.is(headers.has("Content-Type"), true);
37 |
38 | t.deepEqual(Array.from(headers.keys()),
39 | ["accept-encoding", "content-type"]);
40 | t.deepEqual(Array.from(headers.values()),
41 | ["gzip", "application/json"]);
42 | t.deepEqual(Array.from(headers.entries()),
43 | [
44 | ["accept-encoding", "gzip"],
45 | ["content-type", "application/json"]
46 | ]);
47 | });
48 |
49 | test("set with not-string key", (t) => {
50 | const headers = new Headers();
51 |
52 | headers.set(1, "convert-to-number");
53 | t.is(headers.get("1"), "convert-to-number");
54 | t.is(headers.has("1"), true);
55 | });
56 |
57 | test("set with invalid key", (t) => {
58 | const headers = new Headers();
59 |
60 | let message = null;
61 | try {
62 | headers.set("不正なキー", "invalid key");
63 | } catch (e) {
64 | message = e.message;
65 | }
66 | t.is(message, "Invalid character in header field name");
67 | });
68 |
69 | test("delete", (t) => {
70 | const headers = new Headers();
71 |
72 | headers.set("accept-encoding", "gzip");
73 | headers.delete("Accept-Encoding");
74 | t.is(headers.get("accept-encoding"), null);
75 | t.is(headers.has("accept-encoding"), false);
76 | });
77 |
78 | test("init with object", (t) => {
79 | const headers = new Headers({
80 | "accept-encoding": "gzip",
81 | "content-type": "application/json"
82 | });
83 | t.is(headers.get("accept-encoding"), "gzip");
84 | t.is(headers.get("content-type"), "application/json");
85 | });
86 |
87 | test("init with array", (t) => {
88 | const headers = new Headers([
89 | ["accept-encoding", "gzip"],
90 | ["content-type", "application/json"]
91 | ]);
92 | t.is(headers.get("accept-encoding"), "gzip");
93 | t.is(headers.get("content-type"), "application/json");
94 | });
95 |
96 | test("init with Headers", (t) => {
97 | const originalHeaders = new Headers({
98 | "accept-encoding": "gzip",
99 | "content-type": "application/json"
100 | });
101 | const headers = new Headers(originalHeaders);
102 | t.is(headers.get("accept-encoding"), "gzip");
103 | t.is(headers.get("content-type"), "application/json");
104 | });
105 |
106 | test("forEach", (t) => {
107 | const headers = new Headers([
108 | ["accept-encoding", "gzip"],
109 | ["content-type", "application/json"]
110 | ]);
111 |
112 | const entries = [];
113 | headers.forEach((key, value) => {
114 | entries.push([key, value]);
115 | });
116 |
117 | t.deepEqual(entries,
118 | [
119 | ["gzip", "accept-encoding"],
120 | ["application/json", "content-type"]
121 | ]);
122 | });
123 |
124 | test("client mode", (t) => {
125 | const headers = new Headers();
126 |
127 | headers.set("accept-encoding", ["gzip", "br"]);
128 | headers.append("accept", ["application/json"]);
129 | headers.append("accept", ["text/html"]);
130 |
131 | t.is(headers.get("accept-encoding"), "gzip,br");
132 | // Chrome's Headers.append() inserts space after comma
133 | t.is(headers.get("accept"), "application/json, text/html");
134 | });
135 |
136 | test("compatible mode with nodejs's ServerResponse.setHeader()", (t) => {
137 | const headers = new Headers();
138 | headers._clientMode = false;
139 |
140 | // res.setHeader() keeps array as is
141 | headers.set("accept-encoding", ["gzip", "br"]);
142 |
143 | t.deepEqual(headers.get("accept-encoding"), ["gzip", "br"]);
144 | });
145 |
146 | test("convert to server headers", (t) => {
147 | const clientHeaders = new Headers();
148 |
149 | clientHeaders.set("accept-encoding", "gzip");
150 | clientHeaders.set("Content-Type", "application/json");
151 |
152 | const { headers, rawHeaders } = convertToServerHeaders(clientHeaders);
153 |
154 | t.deepEqual(headers,
155 | {
156 | "accept-encoding": "gzip",
157 | "content-type": "application/json"
158 | });
159 | t.deepEqual(rawHeaders,
160 | [
161 | "accept-encoding", "gzip",
162 | "content-type", "application/json"
163 | ]);
164 | });
165 |
166 | test("convert to client mode", (t) => {
167 | const headers = new Headers();
168 | headers._clientMode = false;
169 |
170 | headers.set("accept-encoding", ["gzip", "br"]);
171 | headers.set("Content-Type", "application/json");
172 |
173 | convertToClientMode(headers);
174 |
175 | t.is(headers.get("accept-encoding"), "gzip, br");
176 | t.is(headers.get("content-type"), "application/json");
177 | });
178 |
--------------------------------------------------------------------------------
/test/test-incomingmessage.js:
--------------------------------------------------------------------------------
1 | const test = require("ava");
2 | var StreamTest = require("streamtest");
3 | const { IncomingMessage, bodyKey, textKey } = require("../lib/incomingmessage");
4 |
5 | process.on("uncaughtException", console.dir);
6 |
7 | test("init without url", (t) => {
8 | const req = new IncomingMessage();
9 | t.is(req.url, undefined);
10 | });
11 |
12 | test("init with url", (t) => {
13 | const req = new IncomingMessage("/test");
14 | t.is(req.url, "/test");
15 | t.is(req.method, "GET");
16 | t.is(req.httpVersion, "1.1");
17 | t.deepEqual(req.headers, {});
18 | t.deepEqual(req.rawHeaders, []);
19 | t.deepEqual(req.trailers, {});
20 | t.deepEqual(req.rawTrailers, []);
21 | });
22 |
23 | test("init with method/headers", (t) => {
24 | const req = new IncomingMessage("/test", {
25 | method: "put",
26 | headers: {
27 | "Accept": "application/json"
28 | }
29 | });
30 | t.is(req.url, "/test");
31 | t.is(req.method, "PUT");
32 | t.is(req.httpVersion, "1.1");
33 | t.deepEqual(req.headers, {
34 | "accept": "application/json"
35 | });
36 | t.deepEqual(req.rawHeaders, [
37 | "accept", "application/json"
38 | ]);
39 | t.deepEqual(req.trailers, {});
40 | t.deepEqual(req.rawTrailers, []);
41 | });
42 |
43 | test("init with body", (t) => {
44 | const req = new IncomingMessage("/test", {
45 | method: "post",
46 | headers: {
47 | "content-type": "text/plain"
48 | },
49 | body: "hello world"
50 | });
51 | t.is(req[bodyKey].toString("utf8"), "hello world");
52 | t.is(req[textKey].toString("utf8"), "hello world");
53 | t.is(req.headers["content-type"], "text/plain");
54 | t.is(req.headers["content-length"], "11");
55 | });
56 |
57 | test("init with body without headers (1)", (t) => {
58 | const req = new IncomingMessage("/test", {
59 | method: "post",
60 | body: "hello world"
61 | });
62 | t.is(req[bodyKey].toString("utf8"), "hello world");
63 | t.is(req[textKey].toString("utf8"), "hello world");
64 | t.is(req.headers["content-type"], "text/plain");
65 | t.is(req.headers["content-length"], "11");
66 | });
67 |
68 |
69 | test("init with body without headers (2)", (t) => {
70 | const req = new IncomingMessage("/test", {
71 | method: "post",
72 | body: 10
73 | });
74 | t.is(req[bodyKey].toString("utf8"), "10");
75 | t.is(req.headers["content-type"], "text/plain");
76 | t.is(req.headers["content-length"], "2");
77 | });
78 |
79 | test("init with body without headers (3)", (t) => {
80 | const req = new IncomingMessage("/test", {
81 | method: "post",
82 | headers: {
83 | "cookie": "session=12345"
84 | },
85 | body: 10
86 | });
87 | t.is(req[bodyKey].toString("utf8"), "10");
88 | t.is(req.headers["content-type"], "text/plain");
89 | t.is(req.headers["content-length"], "2");
90 | });
91 |
92 | test("support reader stream interface", (t) => {
93 | const req = new IncomingMessage("/test", {
94 | method: "post",
95 | headers: {
96 | "content-type": "text/plain"
97 | },
98 | body: "hello world"
99 | });
100 |
101 | const called = [];
102 |
103 | t.is(req.listeners("end").length, 0);
104 | t.is(req.listeners("data").length, 0);
105 |
106 | req.on("end", () => {
107 | called.push("end");
108 | });
109 |
110 | req.on("end", () => {
111 | called.push("end2");
112 | });
113 |
114 | req.on("data", (data) => {
115 | called.push("data");
116 | t.is(data.toString("utf8"), "hello world");
117 | });
118 |
119 | t.deepEqual(called, ["data", "end", "end2"]);
120 | t.is(req.listeners("end").length, 2);
121 | t.is(req.listeners("data").length, 1);
122 | });
123 |
124 | test("stream compatibility test", async (t) => {
125 | for (const version of StreamTest.versions) {
126 | const text = await new Promise((resolve, reject) => {
127 | try {
128 | const writer = StreamTest[version].toText((err, text) => {
129 | if (err) {
130 | reject(err);
131 | return;
132 | }
133 | resolve(text);
134 | });
135 | const req = new IncomingMessage("/test", {
136 | method: "post",
137 | headers: {
138 | "content-type": "text/plain"
139 | },
140 | body: "hello world"
141 | });
142 | req.pipe(writer);
143 | } catch (e) {
144 | reject(e);
145 | }
146 | });
147 | t.is(text, "hello world");
148 | }
149 | });
150 |
--------------------------------------------------------------------------------
/test/test-server.js:
--------------------------------------------------------------------------------
1 | const test = require("ava");
2 | const { initFetch } = require("../index");
3 |
4 | function delay(t) {
5 | return new Promise(function(resolve) {
6 | setTimeout(resolve.bind(null), t);
7 | });
8 | }
9 |
10 | const redirectApp = (req, res) => {
11 | res.end("ok");
12 | };
13 |
14 | test("createServer behaves like net/http createServer", async (t) => {
15 | const { createServer } = initFetch();
16 |
17 | const server = createServer(redirectApp);
18 |
19 | t.is(server.listening, false);
20 | let calledListnerCallback = false;
21 | server.listen(80, () => {
22 | calledListnerCallback = true;
23 | });
24 | t.is(server.listening, true);
25 | t.is(server.address().port, 80);
26 |
27 | await delay(50);
28 | t.is(calledListnerCallback, true);
29 |
30 | let calledCloseCallback = false;
31 | server.close(() => {
32 | calledCloseCallback = true;
33 | });
34 |
35 | await delay(50);
36 | t.is(calledCloseCallback, true);
37 | });
38 |
--------------------------------------------------------------------------------
/test/test-serverresponse.js:
--------------------------------------------------------------------------------
1 | const test = require("ava");
2 | const StreamTest = require("streamtest");
3 | const { ServerResponse } = require("../lib/serverresponse");
4 | const { Response } = require("../lib/response");
5 |
6 | test("stream compatibility test v1", async (t) => {
7 | const serverResponse = await new Promise(resolve => {
8 | const response = new ServerResponse(resolve);
9 | StreamTest.v1.fromChunks(["a ", "chunk ", "and ", "another"])
10 | .pipe(response);
11 | });
12 | const response = new Response(serverResponse);
13 | const text = await response.text();
14 | t.is(text, "a chunk and another");
15 | });
16 |
17 | test("stream compatibility test v2", async (t) => {
18 | const serverResponse = await new Promise(resolve => {
19 | const response = new ServerResponse(resolve);
20 | StreamTest.v2.fromChunks(["a ", "chunk ", "and ", "another"])
21 | .pipe(response);
22 | });
23 | const response = new Response(serverResponse);
24 | const text = await response.text();
25 | t.is(text, "a chunk and another");
26 | });
27 |
--------------------------------------------------------------------------------
/test/test-shadow-fetch.js:
--------------------------------------------------------------------------------
1 | const test = require("ava");
2 | const { initFetch } = require("../index");
3 | const fetch = require("../index");
4 | const { printBenchmark } = require("./helper");
5 |
6 | test("the simplest connection and receive regular output", async (t) => {
7 | const { fetch, createServer } = initFetch();
8 |
9 | const server = createServer((req, res) => {
10 | res.writeHead(200, { "Content-Type": "application/json" });
11 | res.end(JSON.stringify({ message: "hello" }));
12 | });
13 |
14 | server.listen(80);
15 |
16 | const start = process.hrtime();
17 | const res = await fetch("/test");
18 | const json = await res.json();
19 | const end = process.hrtime(start);
20 |
21 | t.is(res.status, 200);
22 | t.is(res.statusText, "OK");
23 | t.is(res.ok, true);
24 | t.is(json.message, "hello");
25 |
26 | t.log(printBenchmark("Benchmark shadow fetch and server with standard output", end));
27 | });
28 |
29 | test("the simplest connection and receive JSON output", async (t) => {
30 | const { fetch, createServer } = initFetch();
31 |
32 | const server = createServer((req, res) => {
33 | res.writeHead(200, { "Content-Type": "application/json" });
34 | res.writeJSON({ message: "hello" });
35 | res.end();
36 | });
37 |
38 | server.listen(80);
39 |
40 | const start = process.hrtime();
41 | const res = await fetch("/test");
42 | const json = await res.json();
43 | const end = process.hrtime(start);
44 |
45 | t.is(res.status, 200);
46 | t.is(res.statusText, "OK");
47 | t.is(res.ok, true);
48 | t.is(json.message, "hello");
49 |
50 | t.log(printBenchmark("Benchmark shadow fetch and server with JSON output", end));
51 | });
52 |
53 | test("write chunks and receive text", async (t) => {
54 | const { fetch, createServer } = initFetch();
55 |
56 | const server = createServer((req, res) => {
57 | res.writeHead(200, { "Content-Type": "application/json" });
58 | res.write("hello\n");
59 | res.write("world\n");
60 | res.end();
61 | });
62 |
63 | server.listen(80);
64 |
65 | const res = await fetch("/test");
66 | t.is(res.status, 200);
67 | t.is(res.statusText, "OK");
68 | t.is(res.ok, true);
69 |
70 | const message = await res.text();
71 | t.is(message, "hello\nworld\n");
72 | });
73 |
74 | test("POST method and receive header", async (t) => {
75 | const { fetch, createServer } = initFetch();
76 |
77 | const server = createServer((req, res) => {
78 | t.is(req.method, "POST");
79 | res.writeHead(201, { "Location": "/test/2" });
80 | res.end();
81 | });
82 |
83 | server.listen(80);
84 |
85 | const res = await fetch("/test", { method: "POST" });
86 | t.is(res.status, 201);
87 | t.is(res.statusText, "Created");
88 | t.is(res.ok, true);
89 | t.is(res.headers.get("location"), "/test/2");
90 | });
91 |
92 | const redirectApp = (req, res) => {
93 | if (req.url === "/redirect") {
94 | res.writeHead(302, { "Location": "/landing-page" });
95 | res.end();
96 | } else if (req.url === "/landing-page") {
97 | res.end("landed");
98 | }
99 | };
100 |
101 | test("shadowFetch can follow redirection by default", async (t) => {
102 | const { fetch, createServer } = initFetch();
103 |
104 | const server = createServer(redirectApp);
105 |
106 | server.listen(80);
107 |
108 | const res = await fetch("/redirect");
109 | t.is(res.status, 200);
110 | t.is(res.redirected, true);
111 | const message = await res.text();
112 | t.is(message, "landed");
113 | });
114 |
115 | test("shadowFetch can detect uninitilzed status (1)", async (t) => {
116 | let message = null;
117 | try {
118 | await fetch("/test", { method: "POST" });
119 | } catch (e) {
120 | message = e.message;
121 | }
122 | t.is(typeof message, "string");
123 | });
124 |
125 | test("shadowFetch can detect uninitilzed status (2)", async (t) => {
126 | const { fetch } = initFetch();
127 |
128 | let message = null;
129 | try {
130 | await fetch("/test", { method: "POST" });
131 | } catch (e) {
132 | message = e.message;
133 | }
134 | t.is(typeof message, "string");
135 | });
136 |
--------------------------------------------------------------------------------