Skip to content
Merged
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
7 changes: 6 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
name: deploy

# Deploys cors.sh to Cloudflare: the web app (Next 16 via OpenNext) + the proxy worker.
# Deploys cors.sh to Cloudflare: the web app (Next 16 via OpenNext) + the proxy worker + the
# mock test API (mock.cors.sh, the /playground default target).
#
# Manual-trigger by default so it never fires before the account is configured. To enable
# auto-deploy on merge to main, uncomment the `push:` block below.
Expand Down Expand Up @@ -60,5 +61,9 @@ jobs:
run: pnpm exec wrangler deploy
working-directory: workers/proxy

- name: Deploy mock worker (mock.cors.sh — the /playground default test API)
run: pnpm exec wrangler deploy
working-directory: workers/mock

- name: Deploy web (OpenNext)
run: pnpm --filter web cf:deploy
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ Add `proxy.cors.sh` to your request. For example,
fetch("https://proxy.cors.sh/https://example.com/");
```

## [`cors.sh/playground`](https://cors.sh/playground), The testing environment (forked from hoppscotch)
## [Playground](https://cors.sh/playground)

- [cors.sh/playground](https://cors.sh/playground)
- [gridaco/playground.cors.sh](https://github.com/gridaco/playground.cors.sh)
- [hoppscotch/hoppscotch](https://github.com/hoppscotch/hoppscotch)
A built-in, in-browser CORS tester: paste any URL and watch the **direct** request get blocked by CORS, then watch the **same request through `proxy.cors.sh`** succeed — status, headers, and body, side by side. No API key required.

It defaults to [`mock.cors.sh`](https://mock.cors.sh/no-cors) — our own CORS-reject test API (`/no-cors`, `/wrong-origin`, `/needs-preflight`, `/allow-all`), handy whenever you need an endpoint that reliably _fails_ CORS.

## Projects using CORS.SH

Expand Down Expand Up @@ -52,7 +52,7 @@ See [`DEVELOPMENT.md`](./DEVELOPMENT.md) for the full local setup, [`SPEC.md`](.
Using a cors proxy service to connect to your own server is not a best practice.
We'll consistently optimize our service infra to keep the paid version affordable as possible.

2. The original code behind cors proxy is by Rob wu's cors-anywhere and the playground is forked from hoppscotch. both licensed under MIT, and our project cors.sh is also licensed under MIT License.
2. cors.sh draws its lineage from Rob W's [cors-anywhere](https://github.com/Rob--W/cors-anywhere) (the proxy) and previously shipped a [Hoppscotch](https://github.com/hoppscotch/hoppscotch) fork as its playground — both MIT. The current service is a ground-up rebuild on Cloudflare Workers with its own built-in playground, also MIT-licensed.

## TODOs

Expand Down
3 changes: 3 additions & 0 deletions scripts/stack.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ AUTH_URL=http://localhost:3000
EMAIL_DRY_RUN=1
NEXT_PUBLIC_APP_URL=http://localhost:3000
INTERNAL_SECRET=local_internal
# The /playground page points its requests at the local proxy (prod default: proxy.cors.sh).
PLAYGROUND_PROXY_URL=http://localhost:8786
VARS
# Stripe: dummy by default (webhook-signature tests sign their own events). Override via env
# with a real Stripe TEST key + test price ids to exercise the live checkout/portal calls
Expand All @@ -79,6 +81,7 @@ VARS

echo "[4/5] wait for readiness"
wait_url http://127.0.0.1:3000/api/v1/usage
wait_url http://127.0.0.1:3000/playground
wait_url http://127.0.0.1:8788/allow-all
wait_url http://127.0.0.1:8090/
curl -s -o /dev/null --retry 90 --retry-delay 1 --retry-connrefused http://127.0.0.1:8786/ && echo " up: proxy 8786"
Expand Down
26 changes: 26 additions & 0 deletions tests/e2e/playground.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test, expect } from "@playwright/test";

// The playground page is served by the web app (3000), not the page-origin baseURL (8090).
const WEB = process.env.WEB_URL || "http://localhost:3000";
const MOCK = process.env.MOCK_URL || "http://localhost:8788"; // mock-reject API (workers/mock)

test("playground: direct request is blocked, the same request via the proxy succeeds", async ({
page,
}) => {
await page.goto(`${WEB}/playground`);

// Target a deliberately no-CORS endpoint that is cross-origin to the web app → the browser
// blocks the direct fetch, and the proxy (PLAYGROUND_PROXY_URL → local proxy) rewrites it.
await page.getByLabel("Target URL").fill(`${MOCK}/no-cors`);
await page.getByRole("button", { name: "Run" }).click();

// Direct panel: blocked by the browser.
await expect(page.getByText("Blocked by the browser")).toBeVisible();

// Proxy panel: a 200 means JS could read the response — impossible for a CORS-blocked request.
await expect(page.getByText(/200 OK/)).toBeVisible();

// The proxied response carries the cors.sh-injected header (only the proxy panel has tabs).
await page.getByRole("tab", { name: /headers/i }).click();
await expect(page.getByText("access-control-allow-origin").first()).toBeVisible();
});
2 changes: 2 additions & 0 deletions tests/e2e/playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ export default defineConfig({
{ name: "billing", testMatch: /billing\.spec\.ts/ },
// Opt-in: real Stripe test-mode checkout (skips unless RUN_STRIPE_LIVE + a real key).
{ name: "billing-live", testMatch: /billing-live\.spec\.ts/ },
// The /playground page drives the browser's own fetch() (navigates to the web app, not baseURL).
{ name: "playground", testMatch: /playground\.spec\.ts/ },
// Dashboard suite runs with a real session.
{
name: "authed",
Expand Down
31 changes: 31 additions & 0 deletions web/app/(home)/playground/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import type { Metadata } from "next";
import { Playground } from "@/components/playground";
import { bindings } from "@/lib/control/bindings";

// Reads a runtime binding (proxy base) → must render per request, not at build time.
export const dynamic = "force-dynamic";

export const metadata: Metadata = {
title: "Playground",
description:
"Fire a request from your browser, watch it get blocked by CORS, then watch the same request succeed through the cors.sh proxy. No API key required.",
};

export default function PlaygroundPage() {
// Defaults to the production proxy; e2e overrides it to the local proxy via PLAYGROUND_PROXY_URL.
const proxyBase = bindings().PLAYGROUND_PROXY_URL ?? "https://proxy.cors.sh";

return (
<div className="mx-auto max-w-5xl px-6 py-16 sm:py-20">
<div className="mx-auto max-w-2xl text-center">
<h1 className="text-3xl font-semibold tracking-tight sm:text-4xl">Playground</h1>
<p className="mt-4 text-base text-muted-foreground sm:text-lg">
Fire a request straight from your browser and watch it get blocked by CORS — then watch
the same request sail through{" "}
<code className="font-mono text-foreground">proxy.cors.sh</code>. No key required.
</p>
</div>
<Playground proxyBase={proxyBase} />
</div>
);
}
3 changes: 3 additions & 0 deletions web/cloudflare-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ declare global {
NEXT_PUBLIC_APP_URL?: string;
// Shared secret the proxy presents when pinging the quota-notification endpoint.
INTERNAL_SECRET?: string;
// Proxy base for the /playground page. Defaults to https://proxy.cors.sh; e2e points it
// at the local proxy so the playground test is self-contained.
PLAYGROUND_PROXY_URL?: string;
}
}

Expand Down
Loading
Loading