bolt.diy/app/routes/api.git-proxy.$.ts
Stijnus 0487ed1438
feat: new improvement for the GitHub API Authentication Fix (#1537)
* Add environment variables section to ConnectionsTab and fallback token to git-info

* Add remaining code from original branch

* Import Repo Fix

* refactor the UI

* add a rate limit counter

* Update GithubConnection.tsx

* Update NetlifyConnection.tsx

* fix: ui style

* Sync with upstream and preserve GitHub connection and DataTab fixes

* fix disconnect buttons

* revert commits

* Update api.git-proxy.$.ts

* Update api.git-proxy.$.ts
2025-03-29 21:12:47 +01:00

179 lines
5.0 KiB
TypeScript

import { json } from '@remix-run/cloudflare';
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/cloudflare';
// Allowed headers to forward to the target server
const ALLOW_HEADERS = [
'accept-encoding',
'accept-language',
'accept',
'access-control-allow-origin',
'authorization',
'cache-control',
'connection',
'content-length',
'content-type',
'dnt',
'pragma',
'range',
'referer',
'user-agent',
'x-authorization',
'x-http-method-override',
'x-requested-with',
];
// Headers to expose from the target server's response
const EXPOSE_HEADERS = [
'accept-ranges',
'age',
'cache-control',
'content-length',
'content-language',
'content-type',
'date',
'etag',
'expires',
'last-modified',
'pragma',
'server',
'transfer-encoding',
'vary',
'x-github-request-id',
'x-redirected-url',
];
// Handle all HTTP methods
export async function action({ request, params }: ActionFunctionArgs) {
return handleProxyRequest(request, params['*']);
}
export async function loader({ request, params }: LoaderFunctionArgs) {
return handleProxyRequest(request, params['*']);
}
async function handleProxyRequest(request: Request, path: string | undefined) {
try {
if (!path) {
return json({ error: 'Invalid proxy URL format' }, { status: 400 });
}
// Handle CORS preflight request
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'POST, GET, OPTIONS',
'Access-Control-Allow-Headers': ALLOW_HEADERS.join(', '),
'Access-Control-Expose-Headers': EXPOSE_HEADERS.join(', '),
'Access-Control-Max-Age': '86400',
},
});
}
// Extract domain and remaining path
const parts = path.match(/([^\/]+)\/?(.*)/);
if (!parts) {
return json({ error: 'Invalid path format' }, { status: 400 });
}
const domain = parts[1];
const remainingPath = parts[2] || '';
// Reconstruct the target URL with query parameters
const url = new URL(request.url);
const targetURL = `https://${domain}/${remainingPath}${url.search}`;
console.log('Target URL:', targetURL);
// Filter and prepare headers
const headers = new Headers();
// Only forward allowed headers
for (const header of ALLOW_HEADERS) {
if (request.headers.has(header)) {
headers.set(header, request.headers.get(header)!);
}
}
// Set the host header
headers.set('Host', domain);
// Set Git user agent if not already present
if (!headers.has('user-agent') || !headers.get('user-agent')?.startsWith('git/')) {
headers.set('User-Agent', 'git/@isomorphic-git/cors-proxy');
}
console.log('Request headers:', Object.fromEntries(headers.entries()));
// Prepare fetch options
const fetchOptions: RequestInit = {
method: request.method,
headers,
redirect: 'follow',
};
// Add body for non-GET/HEAD requests
if (!['GET', 'HEAD'].includes(request.method)) {
fetchOptions.body = request.body;
fetchOptions.duplex = 'half';
/*
* Note: duplex property is removed to ensure TypeScript compatibility
* across different environments and versions
*/
}
// Forward the request to the target URL
const response = await fetch(targetURL, fetchOptions);
console.log('Response status:', response.status);
// Create response headers
const responseHeaders = new Headers();
// Add CORS headers
responseHeaders.set('Access-Control-Allow-Origin', '*');
responseHeaders.set('Access-Control-Allow-Methods', 'POST, GET, OPTIONS');
responseHeaders.set('Access-Control-Allow-Headers', ALLOW_HEADERS.join(', '));
responseHeaders.set('Access-Control-Expose-Headers', EXPOSE_HEADERS.join(', '));
// Copy exposed headers from the target response
for (const header of EXPOSE_HEADERS) {
// Skip content-length as we'll use the original response's content-length
if (header === 'content-length') {
continue;
}
if (response.headers.has(header)) {
responseHeaders.set(header, response.headers.get(header)!);
}
}
// If the response was redirected, add the x-redirected-url header
if (response.redirected) {
responseHeaders.set('x-redirected-url', response.url);
}
console.log('Response headers:', Object.fromEntries(responseHeaders.entries()));
// Return the response with the target's body stream piped directly
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers: responseHeaders,
});
} catch (error) {
console.error('Proxy error:', error);
return json(
{
error: 'Proxy error',
message: error instanceof Error ? error.message : 'Unknown error',
url: path ? `https://${path}` : 'Invalid URL',
},
{ status: 500 },
);
}
}