Portfolio — the proxy that closed the door and still saved the form
Portfolio·

Portfolio — the proxy that closed the door and still saved the form

Most problems that look like front-end bugs are, in reality, network bugs.

The Portfolio ran on a hosting domain and the contact form worked. When I started serving it on a custom domain as well, the first impression was that everything stayed the same — same interface, same build, same routes. That was not true. The contact form simply stopped sending messages, the resume download counter dropped out of the database, and visit tracking went to zero. In the browser console, three identical error lines: TypeError: Failed to fetch.

What was actually happening

The form was not broken. It never even got to send the request. The browser runs an automatic check before the send — the preflight — and it came back with an error, missing the authorization header. The browser cancelled right there, before touching anything the person had typed. On the site’s server side there was no record at all: the request never left.

The reason was an explicit list of allowed origins, kept on the service the site talks to. When someone arrived through the new domain, that list did not recognize the origin. Not out of malice — the domain was simply never registered there. And browsers do not distinguish “domain not allowed” from “server down”: for whoever is on the other side of the screen, the difference is always the same three words, Failed to fetch.

This is the kind of failure that never shows up in an automated test. Tests run on the same domain as the API service, where the origin is always the same — the problem only exists when someone arrives through a different path. That is why the fix could not be validated in the wrong place: it needed to be tested from an origin other than the service’s own.

The fix

The solution was to stop asking the browser to cross the domain boundary. Instead, I created an intermediate route on the site itself: the browser only talks to its own origin, and the server talks to the service. When two machines talk to each other, cross-origin policy simply does not apply — the check happens in the browser, and the browser no longer crosses boundaries.

The trick is how the route decides what may pass through. It trusts nothing that arrives in the URL. Each path segment is decoded repeatedly until it stops changing, and only then is it checked: a segment that resolves to . or .., or that contains a backslash, is rejected outright.

~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$

The prefix list is closed on purpose: two paths allowed, nothing else. Without it, the route would be nothing more than an intermediary to any address — changing the proxy’s destination would be a single line of code, and the contact form, the visit tracker, and the messaging service would all hang on the same wire. Closing the list is what separates a “secure proxy” from an “open proxy”.

The detail that almost slipped through

One detail in that validation almost slipped through, and it is the kind of detail that only shows up once you have hit the same problem before.

A few days earlier, a security guard of the project itself — the one that blocks direct downloads of the resume documents — had been bypassed with percent-encoding in the URL. The guard compared the string exactly as it received it, and an encoded value slipped past the comparison while pointing at the same file. It was not a new failure; it was the same lesson, applied somewhere else.

That is why the intermediate route does not decode just once. It decodes in a loop, until the string stabilizes, and only then validates. A path like %252e%252e — which is .%2e, which in turn is .. — sits two levels of encoding deeper, and only becomes fully visible on the final pass. Anyone decoding once was vulnerable to anyone who thought about it.

What changed afterwards

The form went back to sending messages, download counting went back to working, and tracking reappeared on the dashboard. And the bigger gain was architectural: the site no longer depends on configuration spread across another service to function. The allowed-origins list still exists there — but now it protects the service against direct outside calls, instead of being the only thing holding the site up.

A process detail also came out of it. The Portfolio gained its own alarm that checks, on every automated verification round, whether calls between the services still work. It replaces no test — but it catches the regression the same day, before anyone notices on the form that the button stopped responding.

What I would carry to the next project

Two rules. First: never trust an automated test to prove that a cross-domain integration works. Test from a different origin, or the test will pass while the person is looking at an error. Second: when validating an input depends on a string comparison, decode until it stabilizes before comparing. The comparison is the last line of defense — not the first.

The error was not in the interface, nor in the framework, nor in the site’s configuration. It was a new domain nobody updated somewhere else. The fix took an afternoon; what took longer was understanding that the problem was not visible from inside the application.

Terminal widget

The commands below show, straight from the code, the three decisions that hold the proxy up: the destination, the closed list of paths, and the segment validation.

~/lifelog — bash
$cat about.txt
╔══════════════════════════════════════╗
║  Samuel Medeiros                    ║
║  Senior Software Engineer           ║
║  Stack: Python · TypeScript · Rust  ║
║  Projetos: Arachne, Dogwalk,        ║
║            Capivara, TatuEngine      ║
╚══════════════════════════════════════╝
      
$