Skip to content

Commit ff2cb3a

Browse files
committed
feat: handle streaming data
1 parent c1e1f24 commit ff2cb3a

File tree

6 files changed

+107
-75
lines changed

6 files changed

+107
-75
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
.vercel
2+
.wrangler
23
node_modules/
34
package-lock.json

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525

2626
### Vercel
2727

28-
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/lopins/serverless-api-proxy)
28+
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/lopinx/serverless-api-proxy)
2929

3030
### Cloudflare
3131

32-
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/lopins/serverless-api-proxy)
32+
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/lopinx/serverless-api-proxy)
3333

3434
### Netlify
3535

36-
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/lopins/serverless-api-proxy)
36+
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/lopinx/serverless-api-proxy)
3737

3838
## How to use
3939

README_CN.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,15 @@
2525

2626
### Vercel 部署
2727

28-
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/lopins/serverless-api-proxy)
28+
[![Deploy to Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/lopinx/serverless-api-proxy)
2929

3030
### Cloudflare 部署
3131

32-
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/lopins/serverless-api-proxy)
32+
[![Deploy to Cloudflare Workers](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/lopinx/serverless-api-proxy)
3333

3434
### Netlify 部署
3535

36-
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/lopins/serverless-api-proxy)
36+
[![Deploy to Netlify](https://www.netlify.com/img/deploy/button.svg)](https://app.netlify.com/start/deploy?repository=https://github.com/lopinx/serverless-api-proxy)
3737

3838
## 如何使用
3939

package.json

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,12 @@
33
"version": "1.0.0",
44
"description": "Multi-API Proxy Gateway Based on Vercel Routes, Cloudflare Workers, and Netlify Redirects",
55
"scripts": {
6-
"build": "echo 'No build step required.'"
6+
"build": "echo 'No build step required.'",
7+
"test": "echo 'No test step required.'"
78
},
8-
"author": "https://github.com/lopins",
9-
"license": "MIT"
10-
}
9+
"author": "https://github.com/lopinx",
10+
"license": "MIT",
11+
"devDependencies": {
12+
"wrangler": "^3.99.0"
13+
}
14+
}

src/_worker.js

Lines changed: 91 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -4,82 +4,109 @@ addEventListener('fetch', event => {
44

55
async function handleRequest(request) {
66
const url = new URL(request.url);
7-
const queryParams = url.searchParams;
87
const pathname = url.pathname;
98

10-
if (pathname === '/' || pathname === '/index.html') {
11-
return new Response('service is running!', {
12-
status: 200,
13-
headers: {
14-
'Content-Type': 'text/html'
15-
}
16-
});
17-
}
18-
if(pathname === '/favicon.ico') {
19-
return new Response('', {
20-
status: 200,
21-
headers: {
22-
'Content-Type': 'image/png'
23-
}
24-
});
25-
}
26-
if(pathname === '/robots.txt') {
27-
return new Response('User-agent: *\nDisallow: /', {
28-
status: 200,
29-
headers: {
30-
'Content-Type': 'text/plain'
31-
}
32-
});
33-
}
9+
// Define CORS headers
10+
const corsHeaders = {
11+
'Access-Control-Allow-Origin': '*',
12+
'Access-Control-Allow-Methods': '*',
13+
'Access-Control-Allow-Headers': '*',
14+
'Access-Control-Allow-Credentials': 'true'
15+
};
3416

35-
const apiMapping = {
36-
'/discord': 'https://discord.com/api',
37-
'/telegram': 'https://api.telegram.org',
38-
'/openai': 'https://api.openai.com',
39-
'/claude': 'https://api.anthropic.com',
40-
'/gemini': 'https://generativelanguage.googleapis.com',
41-
'/meta': 'https://www.meta.ai/api',
42-
'/groq': 'https://api.groq.com',
43-
'/x': 'https://api.x.ai',
44-
'/cohere': 'https://api.cohere.ai',
45-
'/huggingface': 'https://api-inference.huggingface.co',
46-
'/together': 'https://api.together.xyz',
47-
'/novita': 'https://api.novita.ai',
48-
'/portkey': 'https://api.portkey.ai',
49-
'/fireworks': 'https://api.fireworks.ai',
50-
'/openrouter': 'https://openrouter.ai/api'
17+
// Define static responses
18+
const staticResponses = new Map([
19+
['/', { content: 'service is running!', type: 'text/html' }],
20+
['/index.html', { content: 'service is running!', type: 'text/html' }],
21+
['/favicon.ico', { content: '', type: 'image/png' }],
22+
['/robots.txt', { content: 'User-agent: *\nDisallow: /', type: 'text/plain' }]
23+
]);
24+
25+
// Define API endpoints
26+
const apis = new Map([
27+
['/discord', 'https://discord.com/api'],
28+
['/telegram', 'https://api.telegram.org'],
29+
['/openai', 'https://api.openai.com'],
30+
['/claude', 'https://api.anthropic.com'],
31+
['/gemini', 'https://generativelanguage.googleapis.com'],
32+
['/meta', 'https://www.meta.ai/api'],
33+
['/groq', 'https://api.groq.com'],
34+
['/x', 'https://api.x.ai'],
35+
['/cohere', 'https://api.cohere.ai'],
36+
['/huggingface', 'https://api-inference.huggingface.co'],
37+
['/together', 'https://api.together.xyz'],
38+
['/novita', 'https://api.novita.ai'],
39+
['/portkey', 'https://api.portkey.ai'],
40+
['/fireworks', 'https://api.fireworks.ai'],
41+
['/openrouter', 'https://openrouter.ai/api']
42+
]);
43+
44+
// Handle root static response
45+
if (staticResponses.has(pathname)) {
46+
const { content, type } = staticResponses.get(pathname);
47+
return new Response(content, { status: 200, headers: { 'Content-Type': type } });
5148
}
52-
53-
const [prefix, rest] = extractPrefixAndRest(pathname, Object.keys(apiMapping));
49+
50+
// Handle OPTIONS request for CORS preflight
51+
if (request.method === 'OPTIONS') return new Response(null, { status: 204, headers: corsHeaders });
52+
53+
// Handle API proxying
54+
const [prefix, rest] = getApiInfo(pathname, apis);
5455
if (prefix) {
55-
const baseApiUrl = apiMapping[prefix];
56-
const targetUrl = new URL(`${baseApiUrl}${rest}`);
57-
queryParams.forEach((value, key) => targetUrl.searchParams.append(key, value));
56+
const targetUrl = new URL(`${prefix}${rest}`);
57+
targetUrl.search = url.search;
58+
59+
// Clone the request to avoid mutating the original request object.
60+
const clonedRequest = request.clone();
5861

5962
try {
60-
const newRequest = new Request(targetUrl, {
61-
method: request.method,
62-
headers: new Headers(request.headers),
63-
body: request.body
64-
});
63+
const response = await fetch(targetUrl, clonedRequest);
64+
let rData = null;
65+
// handle non-streaming data
66+
if (!response.ok || !response.body) {
67+
rData = response.body
68+
}else{
69+
// handle streaming data
70+
rData = new ReadableStream({
71+
async start(controller) {
72+
const reader = response.body.getReader();
73+
try {
74+
while (true) {
75+
const { done, value } = await reader.read();
76+
if (done) break;
77+
controller.enqueue(value);
78+
}
79+
controller.close();
80+
} catch (error) {
81+
controller.error(error);
82+
}
83+
},
84+
cancel() {
85+
// Handle cancellation if necessary
86+
}
87+
});
88+
}
6589

66-
const response = await fetch(newRequest);
67-
const modifiedResponse = new Response(response.body, response);
68-
['Access-Control-Allow-Origin', 'Access-Control-Allow-Methods', 'Access-Control-Allow-Headers'].forEach(header => modifiedResponse.headers.set(header, '*'));
69-
return modifiedResponse;
90+
return new Response(rData, {
91+
status: response.status,
92+
statusText: response.statusText,
93+
headers: { ...Object.fromEntries(response.headers), ...corsHeaders }
94+
});
7095
} catch (error) {
71-
console.error('Failed to fetch:', error);
72-
return new Response('Internal Server Error', { status: 500 });
96+
return new Response('Internal Server Error', { status: 500, headers: corsHeaders });
7397
}
7498
}
75-
return new Response('Not Found', { status: 404 });
99+
100+
// Handle unknown route
101+
return new Response('Not Found', { status: 404, headers: corsHeaders });
76102
}
77103

78-
function extractPrefixAndRest(pathname, prefixes) {
79-
for (const prefix of prefixes) {
80-
if (pathname.startsWith(prefix)) {
81-
return [prefix, pathname.slice(prefix.length)];
82-
}
104+
// Parse API information from pathname
105+
function getApiInfo(pathname, apis) {
106+
for (const [prefix, baseUrl] of apis) {
107+
if (pathname.startsWith(prefix)) {
108+
return [baseUrl, pathname.slice(prefix.length)];
109+
}
83110
}
84111
return [null, null];
85-
}
112+
}

wrangler.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name = "sap"
1+
name = "serverless-api-proxy"
22
main = "src/_worker.js"
33
workers_dev = true
44
compatibility_date = "2024-10-01"

0 commit comments

Comments
 (0)