Koala

Build a GitHub Acceleration Proxy with Vercel for Free

The original motivation for this proxy was pretty simple: I needed a small tool to speed up git clone, but I didn’t want to spend money maintaining a server. Vercel’s free tier is generous on both performance and bandwidth, making it the perfect choice for freeloading.

My requirements:

  1. Forward all requests to https://github.com.
  2. Handle large git clone and push operations without restrictions.
  3. Return a simple usage text when visiting /.
  4. Set up /robots.txt to block search engine indexing.

Reference from @lideming’s https://md.yuuza.net/s/U52dzJZWd

Why TypeScript + Edge?

I didn’t start with TypeScript. As someone comfortable with backend, Go was my first instinct, since Go’s ReverseProxy is straightforward and efficient.

I quickly implemented the first version in Go and deployed it to Vercel. Small-scale testing looked fine.

But as soon as I tried git clone on a slightly larger repo, Vercel’s cloud logs showed 413 Payload Too Large.

Go’s failure was a Serverless architecture limitation

The problem wasn’t the Go code itself, but the environment it ran in.

Vercel’s standard Serverless Function (which is where Go runs) is built on AWS Lambda and similar technologies. To handle a request, it buffers the entire request body before passing it to your code. When you’re transferring a Git data pack of several MB or more, the data hits Vercel’s 4.5MB limit1 before it even reaches your Go handler, returning a 413 error.

This made me realize that to handle large file transfers, I needed to bypass the buffering limit and use streaming instead.

Edge Runtime: Solving 413

Vercel’s Edge Runtime2 is based on Web Standards (like Fetch API and Web Streams), designed for I/O-intensive tasks. It natively supports streaming request and response bodies. Data flows through Edge nodes like water, never accumulating in memory, so the payload size limit simply doesn’t apply.

Since we’re going Edge, TypeScript/JavaScript is the most natural, lightweight choice with the fastest cold start.

Core Edge Runtime Implementation

The core logic lives in api/index.ts. We need to tell Vercel explicitly that this file should run on the Edge Runtime.

// Core: Tell Vercel this must run on Edge Runtime
export const config = {
  runtime: 'edge',
};

export default async function handler(req: Request) {
  const url = new URL(req.url);
  const path = url.pathname;

  // 1. Highest priority route: robots.txt
  if (path === '/robots.txt') {
    return new Response('User-agent: *\nDisallow: /', { status: 200 });
  }

  // 2. Home route: return usage instructions
  if (path === '/') {
    const usageText = `USAGE
----------------
Clone a repo:
  git clone https://github.therainisme.com/<user>/<repo>.git
...
`;
    return new Response(usageText, {
      headers: { 'content-type': 'text/plain; charset=utf-8' },
    });
  }

  // 3. Core proxy forwarding logic
  const targetUrl = new URL(req.url);
  // Fix the target address to ensure forwarding to github.com
  targetUrl.protocol = 'https:';
  targetUrl.hostname = 'github.com';
  targetUrl.port = '';

  const newReq = new Request(targetUrl.toString(), {
    method: req.method,
    headers: req.headers,
    // Key: req.body is a ReadableStream, pass it directly for streaming
    body: req.body,
    redirect: 'manual',
  });

  // Must set the correct Host header, otherwise GitHub will reject the request
  newReq.headers.set('Host', 'github.com');
  // Remove X-Forwarded-For etc. to keep the request clean
  newReq.headers.delete('X-Forwarded-For');

  // Forward the request and return the response
  const resp = await fetch(newReq);

  // Return the Response, ensuring streaming data is passed through correctly
  return new Response(resp.body, {
    status: resp.status,
    statusText: resp.statusText,
    headers: resp.headers,
  });
}

Only proxy requests ending in .git and .png

Important! If we forward all requests, the domain registrar might consider this a phishing site and ban the domain. To avoid this risk, we need path filtering to only allow requests ending in .git or .png:

export default async function handler(req: Request) {
  const url = new URL(req.url);
  const path = url.pathname;

  // robots.txt and home route handling (same as above, omitted)...

  // Important: only proxy requests ending in .git or .png
  if (!path.endsWith('.git') && !path.endsWith('.png')) {
    return new Response('Forbidden: Only .git and .png requests are allowed', {
      status: 403,
      headers: { 'content-type': 'text/plain; charset=utf-8' },
    });
  }

  // Rest of the proxy forwarding logic...
}

Two small deployment hurdles

The code was done, but deploying to Vercel came with a couple issues worth noting:

  1. Missing public directory: Since this is a pure API service, there are no frontend build artifacts in the project root. Vercel might throw Error: No Output Directory named "public" found.
    Fix: Create an empty public folder in the project root with a placeholder.html inside.

  2. Root route hijacked by static files: To fix the above, I placed files in public, but Vercel’s default routing gives static files higher priority than API routes. So visiting / served public/index.html instead of the usage text from my TypeScript handler.
    Fix: Don’t name the home page file index.html. Renaming public/index.html to public/placeholder.html means / has no static file to intercept, and traffic flows to api/index.ts as intended.

Conclusion

Works great! Easy to use!

But it seems to only work well in Shanghai. Based on feedback from friends, Beijing, Shenzhen, and Xi’an still experience lag. Also note Vercel’s Edge Runtime has a 300-second time limit3.