Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add proxy plugin 😎 #9

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions bun-example/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import xior, { merge, delay, buildSortedURL, encodeParams } from 'xior';
import cachePlugin from 'xior/plugins/cache';
import errorRetryPlugin from 'xior/plugins/error-retry';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';
import proxyPlugin from 'xior/plugins/proxy';
import throttlePlugin from 'xior/plugins/throttle';

// console.log(merge, delay, buildSortedURL);
Expand All @@ -16,6 +17,7 @@ instance.plugins.use(errorRetryPlugin({}));
instance.plugins.use(cachePlugin({}));
instance.plugins.use(throttlePlugin({}));
instance.plugins.use(uploadDownloadProgressPlugin({}));
instance.plugins.use(proxyPlugin({}));

instance.plugins.use((adapter) => {
return async (config) => {
Expand Down
2 changes: 2 additions & 0 deletions cloudflare-example/src/xior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import xior from 'xior';
import cachePlugin from 'xior/plugins/cache';
import errorRetryPlugin from 'xior/plugins/error-retry';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';
import proxyPlugin from 'xior/plugins/proxy';
import throttlePlugin from 'xior/plugins/throttle';

export const http = xior.create();
Expand All @@ -10,3 +11,4 @@ http.plugins.use(errorRetryPlugin());
http.plugins.use(throttlePlugin());
http.plugins.use(cachePlugin());
http.plugins.use(uploadDownloadProgressPlugin());
http.plugins.use(proxyPlugin());
2 changes: 2 additions & 0 deletions next-example/app/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import xior from 'xior';
import cachePlugin from 'xior/plugins/cache';
import errorRetryPlugin from 'xior/plugins/error-retry';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';
import proxyPlugin from 'xior/plugins/proxy';
import throttlePlugin from 'xior/plugins/throttle';

export const http = xior.create();
Expand All @@ -10,3 +11,4 @@ http.plugins.use(errorRetryPlugin());
http.plugins.use(throttlePlugin());
http.plugins.use(cachePlugin());
http.plugins.use(uploadDownloadProgressPlugin());
http.plugins.use(proxyPlugin());
9 changes: 8 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@
"import": "./plugins/progress/index.mjs",
"module": "./plugins/progress/index.esm.js"
},
"./plugins/proxy": {
"types": "./plugins/proxy/index.d.ts",
"require": "./plugins/proxy/index.cjs",
"import": "./plugins/proxy/index.mjs",
"module": "./plugins/proxy/index.esm.js"
},
".": {
"types": "./dist/types/index.d.ts",
"require": "./dist/index.cjs",
Expand Down Expand Up @@ -74,7 +80,8 @@
"bunchee": "^4.4.8",
"lfs-auto-track": "^1.1.0",
"isomorphic-unfetch": "^4.0.2",
"promise-polyfill": "^8.3.0"
"promise-polyfill": "^8.3.0",
"express-basic-auth": "^1.2.1"
},
"prettier": {
"printWidth": 100,
Expand Down
16 changes: 16 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

76 changes: 76 additions & 0 deletions src/plugins/proxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { XiorInterceptorRequestConfig, XiorPlugin, XiorRequestConfig } from '../types';
import { isAbsoluteURL } from '../utils';
import { xior } from '../xior';

/** @ts-ignore */
declare module 'xior' {
interface XiorRequestConfig {
proxy?: XiorProxyConfig;
}
}

export interface XiorBasicCredentials {
username: string;
password: string;
}

export interface XiorProxyConfig {
host: string;
protocol?: string;
port?: number;
auth?: XiorBasicCredentials;
}

export default function xiorProxyPlugin(): XiorPlugin {
const proxyInstance = xior.create();
return function proxyPlugin(adapter) {
return async (config) => {
const proxyConfig = (config as { proxy?: XiorProxyConfig }).proxy;

if (proxyConfig && proxyConfig.host) {
// url is not absolute url
// url match with baseURL
let isValidUrlToProxy = true;
let isAbsolute = false;
if (config.url && isAbsoluteURL(config.url)) {
isValidUrlToProxy = !!(config.baseURL && config.url.startsWith(config.baseURL));
isAbsolute = true;
}
if (isValidUrlToProxy) {
const { protocol, host, port, auth } = proxyConfig;
let proxyUrl = '';
if (protocol) {
proxyUrl += `${protocol}://`;
}
proxyUrl += host;
if (port) {
proxyUrl += `:${port}`;
}
const url = isAbsolute
? config.url?.replace(config.baseURL as string, proxyUrl)
: proxyUrl + (config.url || '');

const proxyHeaders: Record<string, string> = {};

if (auth) {
proxyHeaders['Authorization'] = `Basic ${btoa(auth.username + ':' + auth.password)}`;
}

const proxyRequestConfig = {
...config,
_url: '',
url,
proxy: undefined,
baseURL: proxyUrl,
headers: {
...config.headers,
...proxyHeaders,
},
} as XiorInterceptorRequestConfig;
return proxyInstance.request(proxyRequestConfig);
}
}
return adapter(config);
};
};
}
35 changes: 18 additions & 17 deletions src/tests/axios-compatible.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ describe('axios compatible tests', () => {
paramsSerializer(data) {
return JSON.stringify(data);
},
// proxy: {
// protocol: '',
// host: '',
// port: 1,
// },
});
const xiorInstance = xior.create({
withCredentials: true,
Expand Down Expand Up @@ -53,30 +58,26 @@ describe('axios compatible tests', () => {
});

it('should work with baseURL', async () => {
const axiosInstance = axios.create({ baseURL: 'https://github.com' });
const xiorInstance = xior.create({ baseURL: 'https://github.com/' });
const { data } = await axiosInstance.get<string>('/');
const { data: axiorData } = await xiorInstance.get<string>('/');
assert.strictEqual(data.slice(0, 10), axiorData?.slice(0, 10));
assert.strictEqual(data.slice(-10), axiorData?.slice(-10));
const axiosInstance = axios.create({ baseURL });
const xiorInstance = xior.create({ baseURL });
const { data } = await axiosInstance.get('/get');
const { data: axiorData } = await xiorInstance.get('/get');
assert.strictEqual(data.method, axiorData.method);
});

it('should work with params', async () => {
const axiosInstance = axios.create({ baseURL: 'https://api.github.com' });
const xiorInstance = xior.create({ baseURL: 'https://api.github.com/' });
const { data } = await axiosInstance.get<any[]>('/orgs/tsdk-monorepo/repos', {
const axiosInstance = axios.create({ baseURL });
const xiorInstance = xior.create({ baseURL });
const { data } = await axiosInstance.get('/get', {
data: { page: 1 },
params: { page: 100 },
params: { page: 100, size: 100 },
});
const { data: axiorData } = await xiorInstance.get<any[]>('/orgs/tsdk-monorepo/repos', {
const { data: axiorData } = await xiorInstance.get('/get', {
data: { page: 1 },
params: { page: 100 },
params: { page: 100, size: 100 },
});
assert.strictEqual(data.length, 0);
assert.strictEqual(axiorData.length, 0);
});
it('should work with post/delete/put/patch/head/options', () => {
//
assert.strictEqual(data.query.page, axiorData.query.page);
assert.strictEqual(data.query.size, axiorData.query.size);
});
it('should work with timeout', async () => {
const axiosInstance = axios.create({
Expand Down
2 changes: 0 additions & 2 deletions src/tests/plugins/error-retry.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import assert from 'node:assert';
import { after, before, describe, it } from 'node:test';
// @ts-ignore
import stringify from 'qs/lib/stringify';

import xiorErrorRetryPlugin from '../../plugins/error-retry';
import { XiorError } from '../../utils';
Expand Down
68 changes: 68 additions & 0 deletions src/tests/plugins/proxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import assert from 'node:assert';
import { after, before, describe, it } from 'node:test';

import xiorProxyPlugin from '../../plugins/proxy';
import { XiorRequestConfig } from '../../types';
import { xior } from '../../xior';
import { startServer } from '../server';

let close: Function;
const port = 7980;
const baseURL = `http://localhost:${port}`;
before(async () => {
close = await startServer(port);
});

after(async () => {
return close(1);
});

describe('xior proxy plguins tests', () => {
const instance = xior.create({
baseURL,
proxy: { host: 'https://hacker-news.firebaseio.com' },
} as XiorRequestConfig);
instance.plugins.use(xiorProxyPlugin());

it('should work with proxy plugin', async () => {
const { data } = await instance.get('/v0/item/121003.json');
assert.strictEqual(data.id, 121003);
assert.strictEqual(data.time, 1203647620);
assert.strictEqual(data.title, 'Ask HN: The Arc Effect');
});

it('should work with `proxy: undefined`', async () => {
const { data } = await instance.get('/get', { baseURL, proxy: undefined } as XiorRequestConfig);
assert.strictEqual(data.method, 'get');
});

it('should work with basic auth', async () => {
const { data } = await instance.get('/basic-auth', {
baseURL: 'https://hacker-news.firebaseio.com',
proxy: {
host: baseURL,
auth: {
username: 'test',
password: '123456',
},
},
} as XiorRequestConfig);
assert.strictEqual(data.method, 'get');
assert.strictEqual(data.msg, 'ok');
});

it('should work with basic auth: complex characters username/password', async () => {
const { data } = await instance.get('/basic-auth', {
baseURL: 'https://hacker-news.firebaseio.com',
proxy: {
host: baseURL,
auth: {
username: '.(*-?',
password: '.(*-?/',
},
},
} as XiorRequestConfig);
assert.strictEqual(data.method, 'get');
assert.strictEqual(data.msg, 'ok');
});
});
16 changes: 16 additions & 0 deletions src/tests/server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,27 @@
import express from 'express';
import basicAuth from 'express-basic-auth';
import multer from 'multer';

export async function startServer(port: number) {
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());

app.get(
'/basic-auth',
basicAuth({
users: { test: '123456', '.(*-?': '.(*-?/' },
challenge: true,
realm: 'Imb4T3st4pp',
}),
(req, res) => {
res.send({
method: 'get',
msg: 'ok',
});
}
);

(['get', 'post', 'patch', 'put', 'delete', 'head', 'options'] as const).forEach((method) => {
app[method](`/${method}`, (req, res) => {
if (req.headers['x-custom-value'] === 'error') {
Expand Down
2 changes: 1 addition & 1 deletion src/xior.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ export class xior {
return finalPlugin<T>(requestConfig);
}

private async handlerFetch<T>(requestConfig: XiorRequestConfig): Promise<XiorResponse<T>> {
async handlerFetch<T>(requestConfig: XiorRequestConfig): Promise<XiorResponse<T>> {
const { url, method, headers, timeout, signal: reqSignal, data, ...rest } = requestConfig;

/** timeout */
Expand Down
2 changes: 2 additions & 0 deletions vite-example/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import xior, { merge, delay, buildSortedURL } from 'xior';
import cachePlugin from 'xior/plugins/cache';
import errorRetryPlugin from 'xior/plugins/error-retry';
import uploadDownloadProgressPlugin from 'xior/plugins/progress';
import proxyPlugin from 'xior/plugins/proxy';
import throttlePlugin from 'xior/plugins/throttle';

console.log(merge, delay, buildSortedURL);
Expand All @@ -12,6 +13,7 @@ instance.plugins.use(errorRetryPlugin({}));
instance.plugins.use(cachePlugin({}));
instance.plugins.use(throttlePlugin({}));
instance.plugins.use(uploadDownloadProgressPlugin({}));
instance.plugins.use(proxyPlugin());

instance.plugins.use((adapter) => {
return async (config) => {
Expand Down
Loading