fix(plane): call the Gauzy API over loopback from the in-process Plane proxy
Problem
pm.gauzy.co half-loads, with many 500s like:
Upstream API unreachable: connect ETIMEDOUT 188.114.97.5:443; connect ENETUNREACH 2a06:98c1:3120::5:443 - Local (:::0); connect ETIMEDOUT 188.114.96.5:443; ...
The Plane proxy is mounted on the Gauzy API's own HTTP server (/api/plane), but it sent its calls to Gauzy on the public API_BASE_URL (https://api.gauzy.co/api), set in both PlaneProxyService.getDefaultProxyConfig() and PlaneIntegrationService.getConfigForTenant(). So each Plane call went:
browser → Cloudflare → gauzy-api (proxy) → out of the pod → Cloudflare → back in → gauzy-api
The IPs in the error are api.gauzy.co's Cloudflare addresses: the pod could not open that outbound connection. Both IPv4 attempts timed out, and the pod has no IPv6 route. When it does connect, the round trip adds about 0.5–1.5 s per proxied call. The user-properties 400 "Failed to find or create new view properties" is the same failure: both of its calls to Gauzy fail.
Fix
- The proxy now calls this API process over loopback, on the address and port the server actually listens on (
httpServer.address()). A wildcard bind is reached on127.0.0.1/[::1], so there is no DNS, TLS or CDN in the path. PLANE_PROXY_UPSTREAM_URL(new, optional) overrides it, in the same form asAPI_BASE_URL(an/apisuffix is optional). Use it to point at a cluster Service, or at the public URL to restore the old behaviour.- A proxy run outside Gauzy (
@ever-gauzy/plugin-integration-plane-apistandalone) is unaffected: it still uses its ownGAUZY_API_BASE_URL. API_BASE_URLis unchanged everywhere else (emails, OAuth, redirects).- The resolved upstream is logged once:
Plane proxy calls the Gauzy API at http://127.0.0.1:3000/api.
Why loopback is safe
externalBaseApiUrlis only used for the proxy's ownapiFetchcalls. It is never returned to the browser.- Gauzy serves plain HTTP (TLS ends at the ingress), so loopback needs no certificate.
- No recursion: upstream paths are Gauzy routes (
/api/...), not/api/plane/.... - Nothing in core grants extra trust to loopback requests. The loopback hits in
packages/authonly validate OAuth redirect URIs. - Rate limiting: authenticated routes are limited per user (taken from the forwarded Bearer token). Public routes such as email-check and login are limited per IP, and those already shared a single bucket, the cluster's egress IP, because the proxy never forwarded the client IP. Follow-up for the plugin repo: forward the client IP.
Tests
- New
plane-proxy.service.upstream.spec.ts(16 tests). It covers address mapping (wildcard v4/v6, specific v4/v6, unknown address) and override normalisation. It reaches a real server bound to0.0.0.0and to127.0.0.1. Through the service'sresolveConfig, it checks loopback for tenant and tenant-less requests and checks that the override wins. - Fails before the fix: the tenant case received
https://api.gauzy.co/api. - Run:
node --experimental-vm-modules node_modules/jest/bin/jest.js -c packages/plugins/integration-plane/jest.config.ts --runInBand: 2 suites, 23 tests pass. - The plugin's specs were never run in CI; this PR adds a
Test the Plane integration pluginstep tobuild.yml.
Rollout
No env change is needed: the loopback default applies on the next release. After deploy, check the pod log for Plane proxy calls the Gauzy API at http://127.0.0.1:..., and check that pm.gauzy.co has no Upstream API unreachable errors.
🤖 Generated with Claude Code