In development, your frontend runs on localhost:5173 and your API server on localhost:3000. The browser blocks cross-origin requests — that's CORS. Vite's dev proxy solves this by forwarding /api/* requests to the backend, making them look same-origin to the browser:
// vite.config.ts
server: {
proxy: {
'/api': 'http://localhost:3000'
}
}
In production this proxy disappears. The built frontend is just static files (HTML/JS/CSS) — no port, no process. Nginx or a CDN serves them, and reverse-proxies /api/* to the backend the same way Vite did in dev:
user → Nginx :80
├── /api/* → backend :3000
└── /* → dist/ static files
One port from the user's perspective, no CORS issue. The backend port is always real and needed; the frontend "port" only exists during development because Vite's dev server is a live process.
Your Linux box resolves a host to its AAAA (IPv6) record, connects, but the remote isn't actually listening on IPv6. You see telnet hang on an IPv6 address. This happens when your system starts preferring IPv6 over IPv4.
One-off fix — force IPv4 for a single command:
telnet -4 api.z.ai 80
Permanent fix — edit /etc/gai.conf and uncomment this line:
precedence ::ffff:0:0/96 100
This tells getaddrinfo() to return IPv4-mapped addresses with higher precedence than IPv6. Changes take effect immediately — no restart needed. gai.conf is re-read on every getaddrinfo() call, so the next DNS lookup picks up the new rule. Existing connections are unaffected.
If /etc/gai.conf doesn't exist, just create it with that single line.
When posting multipart form data via curl in a bash script, it's tempting to build the body in a variable and pass it with --data-raw "$DATA". On Linux this often works fine. On Windows Git Bash, it silently breaks — the server receives the request but fields like body come through empty.
Two things go wrong at once. First, Windows-style paths (C:\Users\...) passed to bash utilities like tail and file are misread — the backslashes get misinterpreted and the file read returns nothing. Fix that with cygpath:
filename=$(cygpath -u "$1" 2>/dev/null || echo "$1")
Second, even with the right path, storing the body in a shell variable and passing it via --data-raw is unreliable. Shell variable expansion, CRLF handling, and platform-specific curl behavior all interact badly. The body gets mangled before it reaches the wire.
The fix is to bypass variables entirely: write the multipart payload to a temp file and send it with --data-binary @file. Since the post body is already in a file, tail it directly into the temp file instead of slurping it into a variable:
TMPBODY=$(mktemp)
TMPDATA=$(mktemp)
tail -n +2 "$filename" > "$TMPBODY"
{
printf "%s\r\n" "--${BOUNDARY}"
printf "Content-Disposition: form-data; name=\"body\"\r\n\r\n"
cat "$TMPBODY"
printf "\r\n%s\r\n" "--${BOUNDARY}--"
} > "$TMPDATA"
curl ... --data-binary "@$TMPDATA"
rm -f "$TMPBODY" "$TMPDATA"
--data-binary @file sends the exact bytes on disk — no shell expansion, no line ending conversion. Works the same on Linux, macOS, and Windows Git Bash.
If you're using Codex CLI and want to insert a newline in the prompt box without submitting, Shift+Enter won't work. Use Ctrl+J instead.
Ctrl+J is the ASCII control character for line feed (\n). Many terminal TUI libraries interpret it as "insert newline" rather than "submit," which is exactly what Codex CLI's input component does.
Shift+Enter looks like the right key (it works in browser-based chat UIs), but whether it inserts a newline or submits depends entirely on how the TUI library handles raw key events — and Codex CLI's library doesn't treat it as a newline.
So: Ctrl+J to insert a newline, Enter to submit.
autossh has its own connection monitoring that opens an extra port for heartbeats. But this is redundant — SSH already has native keepalive via ServerAliveInterval. The extra port can even cause problems if it's not available on the remote side.
The modern approach: disable autossh's built-in monitoring with -M 0 and let SSH's native heartbeats handle it. When SSH detects a dead connection (after ServerAliveInterval * ServerAliveCountMax seconds of no response), autossh automatically reconnects.
Put everything in ~/.ssh/config:
Host gateway
HostName your-gateway-ip
User ec2-user
ServerAliveInterval 30
ServerAliveCountMax 3
LocalForward 8080 internal-soap-ec2:8080
Then the command reduces to:
autossh -M 0 -Nf gateway
-M 0 is the only autossh-specific flag you need. Everything else — host, user, keepalive, even the tunnel — lives in SSH config where it belongs. ServerAliveInterval 30 sends a heartbeat every 30 seconds through the SSH port itself (no extra ports needed), and ServerAliveCountMax 3 means 90 seconds of silence triggers a disconnect detection.