Custom Domain + Reverse Proxy

I’m trying to set up a reverse proxy in front of my IC canister to intercept a single path and route it to an external server, while all other paths continue to my canister. The specific use case is routing WebSocket traffic (which IC doesn’t natively support) to a separate server, while keeping all other traffic on IC. However, I’m running into SSL issues and redirect loops when trying to put a proxy layer in front of the IC HTTP gateway.

Setup

  • Custom domain: pds.edjcase.com configured with IC custom domain setup
  • DNS records configured per IC documentation:
    • CNAME pds.edjcase.com → pds.edjcase.com.icp1.io
    • TXT _canister-id.pds.edjcase.com → sctyd-5qaaa-aaaag-aa5lq-cai
    • CNAME _acme-challenge.pds.edjcase.com → _acme-challenge.pds.edjcase.com.icp2.io
  • Custom domain registration completed and status is “Available”
  • Domain works perfectly when accessing IC directly

Goal

Use a reverse proxy (Cloudflare Worker) to intercept ONE specific path and route it to an external server, while all other paths continue to the IC canister:

  • /xrpc/com.atproto.sync.subscribeRepos → External WebSocket server
  • All other paths → IC canister

Problem

When I enable the proxy layer (required to intercept traffic), the custom domain stops working:

With DNS record set to “DNS only”:

$ curl -I https://pds.edjcase.com/.well-known/atproto-did
HTTP/2 200
# Works perfectly

With DNS record set to “Proxied”:

$ curl -I https://pds.edjcase.com/.well-known/atproto-did
HTTP/2 526
# SSL certificate error

OR

$ curl -I https://pds.edjcase.com/.well-known/atproto-did
HTTP/2 308
location: https://pds.edjcase.com/.well-known/atproto-did
# Endless redirect loop

Is it possible to use IC custom domains with a reverse proxy in front of the IC HTTP gateway?
And if so what does the http gateway/reverse proxy require to make this work

Any guidance or alternative approaches would be appreciated!

For more context I am working on an atproto PDS canister that requires a websocket connection for the com.atproto.sync.subscribeRepos request, which is needed to propagate data to a relay. So in this case I do not have control over the client and the client does not allow for redirects. So essentially (since the IC doesnt have websocket support) I have to fake the one endpoint to point at a non-ic server, that makes a websocket connection on one end and polls my canister on the other, as glue to fix it. But that doesn’t work if i cant get this problem solved

Hey @Gekctek

I would recommend against using your setup. If you want both Cloudflare and a custom domain, both will need to obtain a valid certificate. This can lead to interference between the two (and the potential failure of successfully obtaining it).

I would recommend you set up some web worker in Cloudflare that simply takes the Websocket traffic and sends it to your external service, while proxying all other requests to the canister ID domain (and not to the custom domain). This proxying can be done transparently such that the user never sees the canister ID domain.

Here is a snippet for a worker that does the proxying to the canister ID domain:

addEventListener("fetch", event => {
  event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
  const url = new URL(request.url);
  
  // Target domain (the one you're showing content from)
  const targetDomain = "https://<CANISTER_ID>.icp0.io";

  // Reconstruct the path to pass through
  const targetUrl = targetDomain + url.pathname + url.search;

  const modifiedRequest = new Request(targetUrl, {
    method: request.method,
    headers: request.headers,
    body: request.body,
    redirect: 'follow'
  });

  // Fetch and return the response
  let response = await fetch(modifiedRequest);

  return new Response(response.body, {
    status: response.status,
    statusText: response.statusText,
    headers: response.headers
  });
}

Just replace <CANISTER_ID> with your canister ID.

Oh thank you, this saved me
Never even though of just bypassing and going straight to the canister id url

I have also had issues with custom domains not being able to do ‘raw’ requests, besides potential security, do you see issues with sending it to the <canister_id>.raw.icp0.io url in this scenario? That would be a game changer for me so i could do query requests (much easier) for the API

Yes, you can also just go to raw. Despite the security issue that you mentioned, I don’t see any other drawbacks of using raw.