How I Fought CORS, HTTP/2, DNS, and Render — and Became a Sysadmin on Accident
- Miranda Griffin

- Jul 5
- 7 min read

I wasn’t trying to become a sysadmin. I was just trying to launch Ember Relay — my investigative AI tool. The concept was simple: click a button, upload a file, get useful insights.
Instead, I got:
CORS errors that made me question reality
A custom domain setup that gaslit me for hours
HTTP/2 protocol errors that only showed up when everything else was working
Tokens that looked valid until they weren’t
And the growing suspicion that maybe, just maybe, I was becoming the person responsible for keeping it all alive
This post is a breakdown of that chaos. If you’re a founder, solo dev, or anyone trying to ship a real product while duct-taping infrastructure together — this is for you.
You will absolutely hit these walls. I hope this post makes it faster and less lonely when you do.
The Stack
I didn’t choose the stack so much as it... accumulated.
Ember Relay started as a solo project to investigate the shady LLC shell structure behind my own apartment complex. I built a quick tool to transcribe audio notes and turn them into structured insights using GPT. Then it spiraled into a full platform.
Here’s what I ended up working with:
Frontend: React (with Vite), TailwindCSS, Axios, and more fetch() than I’d like to admit.
Backend: Flask, SQLite (don’t @ me), JWT, OpenAI’s Whisper + GPT-4, and Flask-CORS.
Deployment: Render. It was supposed to make things “easy.” That was adorable.
Auth: Clerk — which is great until you try to get the actual token you need to pass into your headers, then it becomes a minor existential puzzle.
Storage: Local file storage in /data/user/session/audio.m4a. No S3, no fancy pipelines. Just me and the filesystem.
Other Tools: GitHub, VS Code, Google, and occasionally yelling at a terminal window in Starbucks.
At the start, I had a decent grip on Python and some MySQL, but I wasn’t what I’d call “production ready.” Definitely not a full-stack dev. Definitely not a sysadmin. Not yet. This stack worked until it didn’t — and when it broke, it broke in the kind of ways that make you question whether software should even be allowed to exist.
The First Wall – CORS
If you’ve never fought with CORS before, congratulations. May your life remain pure. For the rest of us, CORS is like a security guard who stands between your frontend and backend, arms crossed, demanding to know exactly who you are and whether you're allowed to be there — even though you literally built both sides of the damn app.
I had the backend running on https://api.emberrelay.com and the frontend on https://www.emberrelay.com. I sent a request from React to Flask and got smacked with:
Access to fetch at 'https://api.emberrelay.com/api/transcribe' from origin 'https://www.emberrelay.com' has been blocked by CORS policy...
Cool. Thanks.
So, I did what any reasonable dev does:
Googled the error
Installed flask-cors
Added CORS(app) to app.py
Prayed
Didn’t work.
Then I learned that CORS errors don’t always show up in the backend logs, which is fun. They only show up in the browser console, like a little passive-aggressive post-it note left on your code. Turns out, I had to do this instead:

I was trying to POST audio data with an auth header and no cookies, which means it triggered a preflight OPTIONS request. Flask wasn’t ready for that. It just silently dropped the request and moved on with its life while I screamed into the console.
CORS errors are like bureaucratic nonsense — the kind of problem where everything looks like it should work, but someone’s rubber stamp is missing, and no one will tell you whose. Once I got it fixed, I thought I was done. I had no idea it was just the beginning.
The Phantom Protocol – When HTTP/2 Gaslit My Dev Console
So there I was, proud of myself for finally fixing the CORS issue and getting my Flask backend to talk to my frontend like two coworkers who used to hate each other but now nod politely in the hallway.
And then… it broke. Again. But not consistently.
Sometimes my API calls worked fine. Sometimes they didn’t. Sometimes the preflight request ghosted me completely. And then I saw it. A clue. A whisper from the void: net::ERR_HTTP2_PROTOCOL_ERROR
Excuse me? What even is that? I started digging. Like every cursed system mystery, the problem wasn’t with my code. It was with the protocol itself — or more accurately, the way Render and Microsoft Edge (yeah, well now I know!) were interpreting HTTP/2 under certain conditions.
You know what doesn’t play well with low-level misconfigurations and insecure fallback headers? HTTP/2. You know what doesn’t tell you where it hurts? HTTP/2.
I didn't want to know what "stream 1 was not closed cleanly" meant. I just wanted to POST a damn file.
Eventually, I found enough half-helpful GitHub issues and doom posts on Stack Overflow to understand:
Render uses HTTP/2 by default (good for speed)
Flask’s built-in dev server doesn’t (good for sanity)
HTTP/2 and misconfigured CORS headers = gaslighting
Fix? I stopped trying to "fix" HTTP/2 and instead focused on controlling the headers tightly from the server side. I made damn sure every request had a proper "Access-Control-Allow-*" trail, and I used curl like a detective who doesn’t trust eyewitnesses.
This is also where I started logging everything. Like a paranoid squirrel hiding receipts in case the tree burns down. By the time it was over, I still didn’t fully trust HTTP/2. But I did trust myself. I had moved one level deeper into the infrastructure. I had become just technical enough to be dangerous.
DNS, CNAMEs, and the Wix Fiasco
If HTTP/2 was the mysterious ghost in the machine, DNS was the gremlin hiding in the basement, chewing on wires and grinning. I had a domain. I had a backend. I had a frontend. What I didn’t have was a clue how to connect them all in a way that didn’t result in random 404s, SSL errors, and silent failures that made me question reality.
I bought the domain name specifically for this project, thinking, “How hard could it be to point a few records and be done with it?”
Spoiler: Wix is built to keep you inside Wix. Like a glitter-covered walled garden that charges you $300/year to escape.
Their DNS panel is… not intuitive. Want to add a custom CNAME or A record? Sure, but first you’ll need to:
Navigate six menus deep.
Decode vaguely labeled dropdowns.
Guess which record controls what.
Hope it doesn't override something silently.
After some trial, error, and accidental hostage-taking of my own subdomain, I finally got the www pointing to my frontend (Render), and api pointing to my backend. Except… nothing worked.
It turned out I had a typo in one of the CNAME entries. Just one character off. It broke everything, silently, and since DNS propagation takes time, I kept “fixing” the wrong thing. Eventually I fixed it — after hours of backtracking, DNS flushes, and panic-refreshing every DNS lookup tool on the internet like I was waiting for a pregnancy test, but by then, the damage was done. I had become the one thing I never thought I’d be. A sysadmin.
The Final Boss – Render Deployments and Gunicorn Nightmares
With DNS finally wrangled into submission and the CORS-poltergeist mostly exorcised, I was feeling good. Swaggering, even. I had a backend deployed on Render, a frontend that could talk to it, and a domain name that pointed to both without tears.
And then came Render's build logs — a special flavor of cryptic. Sometimes, Render would just decide it didn’t feel like deploying today. My logs would say things like:
==> Starting service...
==> Exited with status 1
Why? No idea. The logs gave nothing. Just vibes.
When it wasn’t failing silently, it was screaming in all caps because Gunicorn, my production server, was misconfigured. I had misspelled the app reference once (app:ap), and another time I somehow managed to invent a syntax error that didn’t exist in local dev. My favorite? The one where I forgot to import the app into init.py and it just... failed to boot. Deploying to Render feels like having a very polite robot assistant who nods, takes your code, and then walks off a cliff — without telling you why.
Eventually, I got it working by:
Manually specifying gunicorn app:app (and triple-checking the spelling)
Making sure my app was exposed at the root of the right file
Turning off auto-builds because I didn’t trust the timing
Keeping the render.yaml file guarded like a sacred scroll
Render’s caching also tried to gaslight me a few times. I would change code and nothing would update — because it was serving a cached version from some deployment two days ago. Eventually I learned to hit Clear Build Cache like I was slapping a broken TV.
But… it worked. In the end, the site deployed, the app ran, the endpoints responded, and the logs finally shut up.
But I was doing sysadmin work. I was deploying, debugging, maintaining infrastructure. Whether I liked it or not, the role had chosen me.
The Accidental Promotion – Becoming a Sysadmin
I didn’t set out to become a sysadmin. I just wanted to build a tool. Something that would transcribe audio, generate insights, and help with investigations. Something simple.
The moment I bought a domain, I stepped onto a trapdoor that dropped me into the underbelly of modern web infrastructure. Suddenly, I was maintaining config files, debugging invisible headers, and speaking the cursed dialect of deployment logs. Every fix opened another layer. Every solution required a tool I hadn’t meant to learn.
Before long, I could:
Parse DNS propagation times in my sleep
Understand the difference between a 403 caused by CORS vs. one caused by a proxy
Explain why gunicorn app:app works, but gunicorn flask:app doesn’t — with examples
Log everything just in case the logs themselves disappeared
Ping myself in curl for therapy
And honestly? I'm proud of it. Not because I enjoy fighting with HTTP protocols, or because Render ever stopped stressing me out, but because I didn’t let the tech stack win. I wrestled it into submission. I learned what I had to learn. I adapted. Somewhere along the way, I earned a new title. Not “frontend dev.” Not “Python hobbyist.” Not “freelancer.” Sysadmin. By accident. By force. By fire.



Comments