DEV Community

Cover image for I meant to ship a 2.3. It became a 3.0.
itsuki
itsuki

Posted on

I meant to ship a 2.3. It became a 3.0.


Runique is a Django-inspired web framework in Rust that I'm building on my own. Axum, SeaORM, Tera. Version 3.0 just shipped, and I hadn't planned it.

It started as a 2.3. One more release, a few features, some fixes. Then the list of changes kept growing. One day I counted the breaking changes: about thirty. Changed signatures, removed functions, a single user model instead of three, NOT NULL columns by default in the model DSL.

Calling that a 2.3 would have been lying to the people using it. A minor update shouldn't break an existing project. This one did. So I renamed it, wrote a migration guide, and owned it.

What changed

I won't list everything, the CHANGELOG does that better than I can. Here's what matters most to me.

Security first. Twenty-one fixes in 3.0 alone. Some still make me wince. Password reset links were built from the request's Host header, so an attacker could have a victim's token sent to their own site. The admin wrote every key of the request body, including ones it never asked for. Deactivating an account didn't close its open sessions. All of that is fixed, with tests that fail without the fix.

One user model. There used to be several, depending on whether you were in the admin or in the app. Now there's one, eihwaz_users, and rights are read from the database on every request. No more cache holding on to a right that was taken away.

Forms you can't forget to validate. ValidationForm<F> can only be obtained by validating the form. If the handler has the value, validation happened. The compiler guarantees it, not my attention span.

A real testing tool. runique test runs business logic against a real database, inside a transaction that is always rolled back.

The hardest part

It wasn't one bug in particular. It was holding on.

Every fix uncovered another one. I'd run cargo-mutants and find tests that checked nothing. I'd fix them, run it again, and there'd still be more. I shipped 3.0.0 and found a gap in the runique new scaffold. I shipped 3.0.1, then 3.0.2 for upload and redirect vulnerabilities.

At some point you start wondering if it'll ever end. If the project is too big for one person. I won't pretend that thought never crossed my mind.

What I'm proud of

Not giving up.

Not because it's done. It isn't. After 3.0.2, a comment on Reddit pushed me to compare my redirect guard with a real URL parser. It found three more bypasses. They're fixed, tested, and will go out in the next release.

And the biggest rework is still ahead of me: replacing the admin's generated code with a typed builder, where a column that doesn't exist doesn't compile.

But I've learned to see these findings differently. A vulnerability I find is one a user won't find for me. The project isn't weaker because I keep finding problems. It would be weaker if I stopped looking.

What's next

I cut my roadmap down. It used to list everything Runique might do one day. Now it says what I'm working on, and what "finished" means to me: tested on both sides, run through mutants, checked for real, documented. Few things at a time, but finished.

If you're on Runique 2.x, the migration guide covers every breaking change with a before/after.

And if you find a vulnerability, I'd rather hear it from you than from someone else: SECURITY.md.


Written in French, translated with AI help.

Runique: GitHub · runique.io · crates.io

Top comments (0)