Hacker News new | past | comments | ask | show | jobs | submit login
A heck of a wild bug chase (georgemauer.net)
66 points by togakangaroo 5 months ago | hide | past | favorite | 56 comments



It seems to me like the underlying issue was ignoring HTTP semantics and making a state-changing link like a logout link a plain <a> (HTTP GET) and not something like a form submission (HTTP POST).

Having intuition for the foundational layers of our tools saves so much time and future headaches.


Author of the post here,

There was no form submission, I'm not sure where you got that. There was also no POST. Though yes, I agree that in the core HTTP semantic, you wouldn't want to change state on a GET and that should include not calling `Set-Cookie`. And yet the reality is that that nearly every application - and many popular libraries like auth0 - do in fact set and clear cookies on `GET`.

The issue here was that the `Link` component in NextJs

- does preloading by default (which is a bad idea exactly for the above reason of reality being different from theory)

- doesn't do preloading by default when running on the dev server (so you don't see the error until its deployed)

- because it does preloading directly in javascript, it can't possibly follow the HTTP semantic of not actually applying cookies until later when the cached route is used

Everything else was the wild goose chase bits.

Also I asked claude to criticize the article as a web forum might before publishing, and this is definitely the tone it gave :D

Oh, also, I'm pretty sure I got the part wrong where i was talking about the preload attribute in HTML, but so far no one's noticed. I should correct that.


> There was no form submission, I'm not sure where you got that. There was also no POST.

OP was saying the logout function should have been behind a form submission / POST.


Ah, yes, I mean, agree that would have been technically correct, but like I said, its just not how a lot of the web works. auth0-nextjs seems to react to `GET` by default (though it might also work with `POST` and you certainly can override things)


So OP was correct that a proper use of the foundational layer of HTTP would have saved time, yours in particular, right?

Also, I didn’t get your ”Claude predicted your tone smiley” thing. OP tone seemed polite and clear. Your tone, on the other hand, seemed defensive and dismissive. Even after you realizing that you initially misunderstood what OP said, adding a “I mean” and a “but I like I said” to reinforce you were right even while misreading what OP said (rather than just acknowledging you got it wrong in the first reading).

I would go even further and speculate that you were predisposed to get a dismissive tone from a web forum (your previous Claude test suggests that) so much that you got a perfectly fine comment and misread in a way that it felt in the “wrong tone” to you. Even misunderstanding what the post said. All of that to confirm your predisposition.


I think I left my context collapse a little here. The article had gotten really good feedback when I passed it around in the various communities I'm in, but I hadn't written it with the idea of the broader hackersphere in mind. I did post the story to here, but I didn't really think it would get traction. I should have done some double-checking and added caveats and context beforehand.

My comment about Claude was simply intended to giggle at how much it has us pegged, not to call out the op directly.


It's well beyond "technically correct", especially for the web. The "safe methods" section is quite explicit about that:

https://www.rfc-editor.org/rfc/rfc7231#section-4.2.1

https://www.rfc-editor.org/rfc/rfc9110#name-safe-methods

By electing to actionably mutate state on GET, one subscribes themselves to a world of hurt.

It is totally how the web works, both as defined by HTTP and in practice. Surely one can pile a dozen workarounds to circumvent the GET safety definition, but then it's just flat out simpler to have it be a POST or DELETE and work as intended.

That a lot of people are doing it a certain - broken - way certainly does not mean they are right.


That would also have been practically correct, avoiding you this bug and the many hours of debugging, being resilient to byzantine/adversarial technologies (NextJS reimplementing prefetching itself and making debugging very difficult)


"because it does preloading directly in javascript, it can't possibly follow the HTTP semantic of not actually applying cookies until later when the cached route is used"

I may be wrong, but I don't think using JavaScript vs using the standard HTML <link> element to prefetch makes a difference here. I don't see anything in the HTML specs about preload or prefetch delaying cookie setting to sometime after the resource is actually loaded (although admittedly I find this bit of the spec somewhat hard to read, as it's dense with references to other parts of the spec). I tried it out, and, both Firefox and Chrome set the cookies for preloaded and prefetched links when the resource is loaded, even if the resource is never actually used.


> Also I asked claude to criticize the article as a web forum might before publishing, and this is definitely the tone it gave :D

Come on.


This is a very good example where the HTML extensions that alex proposed here:

https://www.youtube.com/watch?v=inRB6ull5WQ

(TLDW: allow buttons to make HTTP requests; allow buttons & forms to issue PUT, PATCH & DELETE; allow buttons, forms & links to target elements in the DOM by id instead of only iframes)

would improve the web platform. You could have a stand-alone logout button that issues a DELETE to /session or whatever. Nice and clean.


I mean, it should just be a submit for a form with a /logout POST action. It’s standard and what web devs have been doing for decades


Yeah, the problem is that it requires a form, which has layout implications w/o styling and POST is not idempotent, whereas a logout operation typically is idempotent. Being able to issue a DELETE to a URL like /session from an element that doesn't have layout implications would be ideal.


A button doesn't have to be inside a form, though. You could have an empty form as a neighbour to the button (or anywhere else inside the page body), and associate the button with it.

  <button form="logout-form" ...>logout</button>
  <form name="logout-form"></form>
No layout implications that way, barring any nth-child css (solvable by putting the form somewhere else). Doesn't solve the form being limited to GET/POST, but styling concerns are atleast handled.


doable but rarely used, inconvenient and awkward, alex proposes allowing buttons to be stand-alone hypermedia controls which also allows multiple buttons located within a form to perform different actions (e.g. save v. cancel)


Oh for sure, standalone elements would definitely be better, I just wanted to point out that there's a way around needing to do silly stuff like <form class=blabla>

Though in my experience, it's great in frameworks like svelte. Define your forms at the top of the component, and you can see at a glance what native actions the component can do, and where it posts to.


> POST is not idempotent

It certainly can be, there’s just no _requirement_ that it is idempotent. There’s no problem with having an idempotent POST operation.

I’m also not a fan of a DELETE /session option, the client shouldn’t have to care about the concept of “session”, it’s the server’s problem. But I’m not a fan of resource-based endpoints in general, I tend to prefer task-based endpoint so /logout makes more sense to me.

Also, even if it was possible to trigger DELETE in html it probably would be implemented as a form. Not really a problem, making a form element inline is trivial, and probably needed in various parts of an app (any button that changes some state)


right, but the browser doesn't know that and so it has to treat the operation as if it were not idempotent (i.e. warn on a resubmit)

You second paragraph indicates that you do not like the REST pattern of the web, which is fine, but i hope you can appreciate that some of us would like the web as the web to make it possible to abide by that pattern

the last point is addressed in Alex's proposal to allow buttons to function as stand-alone hypermedia controls


Genuine question: How do you believe one should learn these semantics? This is more something I've been pondering myself recently, because I agree with you that the foundational knowledge for our work in any tech stack is usually the most important for understanding higher abstractions. But with so much to know it feels impossible to 'know it all' so to speak especially if you wear more than one specialized hat. Then beyond that even if you're only trying to learn just the foundations how do you know what those foundations are if you're not already inundated in that stack?

This is mostly just my personal ramblings, but I'd be curious other peoples viewpoints on this.


I remember many years ago when I used to read print magazines about programming and web development.

One of those magazines told a story about a web site that had lost a lot of data. What had happened? Well, somehow they had this page that

1. Required no authentication at all, and

2. Was using links like

  <a href="/path/to/file?action=delete>Delete file</a>
And so the Google web crawler had come across this page and happily visited each and every one of those links.

That’s when I learned about the importance of using forms with POST requests for certain actions instead of using links that send GET requests.

And then some years later someone told me about this thing called HATEOAS and about RESTful APIs and that actually there are different HTTP verbs you can use other than just GET and POST. Like for example

  DELETE /path/to/file
As for your question about how someone is supposed to learn that these days?

Ideally whatever web development tutorials or courses or books they are using would at some point tell them about the different HTTP verbs that exists, and of how and when to use each of them, and crucially to tell them about bad consequences of using GET for anything that has side-effects like logging out a session or deleting a file.


This can be complex sometimes, but in case of HTTP methods specifically, it's hard to imagine how one can't know about this.

You learn HTML (and see a mention of "POST" method); or read HTTP primer (and see reference to methods) or open browser inspection window and see prominent "Method" column, or see the reference in some other place. You get interested and want to look it up - say wikipedia is often a good start [0] for generic part. And the second sentence of description says it all:

> GET: The GET method requests that the target resource transfer a representation of its state. GET requests should only retrieve data and should have no other effect.

[0] https://en.wikipedia.org/wiki/HTTP#Request_methods


MDN, no matter how highly rated, is still insanely underrated.

https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods

Also the many HTTP RFCs, this one in particular covers semantics:

https://www.rfc-editor.org/rfc/rfc9110.html

As the age old wisdom says... RTFM :P

HTTP is awesome, I'm in love with it. Beautiful piece of work.


IMO it's very understandable to not know about this sort of thing starting out. Everybody was new once, and it's much easier to get motivated to build cool stuff than to read about all the fine details of all of the technologies we're using. I say, go ahead and take the shortcuts and build some cool things in a maybe sloppy way as long as the traffic and stakes aren't too high. Then, once you've got something cool built, take some time every now and then to seek out and read about more of the details of some of the systems and tools you're using.


While it may not be quite the same answer you're looking for, I'd suggest the OWASP, and at least their top 10 for sure. Learning about SSRF may not have stopped this behavior (it's coming from the authenticated browser), but if you're doing CSRF checks you won't get logged out by random links on other peoples sites, and that whatever logged you out was a legitimate action.


Personally, I think it comes from experience and learning. I read the comment and an old HN story popped into my head.

That was where I "learnt" side effects of not using verbs properly. It stuck to me from then.

https://news.ycombinator.com/item?id=16964907


It requires slowing down. Unheard of.


Exactly. And ditching the "move fast and break things" mindset. Learn your craft and embrace the learning process. Always be curious about how the stuff below your layer works, fundamentally. Recurse on searching for the seminal works that defined those layers.

This seems appropriately relevant today: https://news.ycombinator.com/item?id=41208627

We (the industry) have built up so many layers upon layers and frameworks designed to make things easier that it just seems to attract newcomers to software engineering with this mindset that all it takes is to start with the sample-app for a high level framework, hack on it with trial and error until it does something they want, and then take to social media with proclamations of "Look! I built a thing! You can hire me to build your thing now!"


A book is always a good start.


The RFCs are often fairly well written and not so hard to digest.


Follow the Ruby on Rails getting started guide and build a toy Rails web app. It has conventional http semantics baked in, you'll learn a lot.


To be fair, <a> tags can't send out non-GET requests. Which yes, can be interpreted as "logout controls should be buttons in forms, not links", but I would really like native htmx-like attributes for intractable html elements.


What’s stopping you from using htmx with a form? Hx-post works as a form element attribute just fine.


Nothing. I just think it logically makes sense as a native feature.


> I didn’t want to deal with databases.

So instead I used a third party authentication service, store some data in JSON files, and also threw up a lamda gateway to store some more data in Google Sheets?

It's not relevant to the bug hunt, but I'm genuinely intrigued. Is this approach considered easier to work with than using a regular ol' DB?


My first thought as well. This is the most complicated stack I’ve seen for something so simple. Just convinced me more to avoid using JavaScript as a backend


Honest question: how is that JS's fault?


> It is wild how decisions made in one part of a technical stack can manifest at another point in time, in another place in the stack, and in such a convoluted manner.

While this isn't something that _only_ happens in modern javascript, it certainly is a pattern. These bugs are convoluted and difficult to debug because the technologies are convoluted, stacked on top of each other compounding the bugs, and devs do not understand the underlying platform.

Honestly, it's once-again a good case study for why I'd stay from NextJS or "modern JS" devs (despite being in this ecosystem myself, Node/React/RN):

- NextJS, one of the most modern techs in wide use, makes debugging _harder_ than vanilla JS?? This is crazy - Reimplementing browser behaviour in JS is _exactly_ what I would expect to be the root cause of various difficult-to-debug-and-to-fix bugs down the line - Using GET for a logout is a misunderstanding of HTTP semantics. This could have broken in other ways (eg integrating turbolinks in the app).

Well done for debugging and fixing this, but honestly... this doesn't speak to the strength of the technology choices or the author's understanding of the platform


I always do <a href="#" id="logout">logout</a> so there is no url to accidentally GET call anyways.


This is the worst of both worlds, it doesn't conform to the spec and it has incorrect semantics. See HTMHell [0] for a brief discussion on the topic, albeit with a slightly different example.

[0] https://www.htmhell.dev/8-anchor-tag-used-as-button/


ok fine <a href="/" id="logout">logout</a> but at least / is safe to GET


Why make it a link if you aren't linking to anything?


so you don't have to set "cursor: pointer" in css


You should really use a button for this. Using anything else is semantically incorrect, and the "it comes with bad styles" argument doesn't make sense given a CSS reset is under 10 lines of code, or even less if you use the `all` style.


And it gives some baseline accessibility functionality. Sucks that there isn't any gerneral "I am clickable" element that neither has bad default click behavior, nor has its own likely-unfitting styling.


What's wrong with a button? You can make it look like an <a> can't you?


https://stackoverflow.com/questions/1367409 has varying amounts of CSS for that, ranging from bad (i.e. more than one line, i.e. the bar to beat for people to automatically use it over <a>) to horrific.

Looks like the rough minimum is this:

    background: none;
    border: none;
    padding: 0;
    cursor: pointer;
    font: inherit;
And, given that some answers there have browser-specific additions, it looks like the spec doesn't have sane effective restrictions on what funky things browsers can do, thus essentially making the answer a "no, you cannot portably make a button look exactly like a link".

And no CSS I could find made the button content flow inline with surrounding text (I even went through all of the computed CSS in FF devtools and added the differing ones to the button), so it looks like the answer is "no" in practice too. (though, granted, for a good amount of link-buttons out there you'd probably want no mid-button breaking)

Certainly looks like it'd be simpler to make a <span> act like a link, than a <button>, at least. At which point you lose the implicit accessibility functionality.


why avoid so hard to make it a `submit` for a `<form method="POST" action="/logout">`, which is semantically correct and removes all the need for any javascript (logouts have never needed JS after all)?


because you often need to send a DELETE verb not POST


"If not link, why link shaped?"


Authentication is a major hurdle. Back in the days of desktop software, there was no authentication. Mobile apps can often avoid authentication too. Despite decades of web coding, and lots of "this authentication system will make life easy" claims, it is still hard and easy to mess up.


it's impossible to avoid authentication once you start sharing data between systems though. This includes basic stuff like "I only want to share the list with our graduates", as well things like "those preferences should be the same on every device which user X owns".


As my former boss at Sun, Scott McNeely said "the network is the computer"


OP learnt about idempotency in http the hard way. However the post reminded me that I don't particularly like this hydration step of NextJS apps where there is javascript executing that is difficult if not impossible to step debug.


So would this simply work if the Link component from nextjs wasn't used?


Yes but the underlying problem would still be there, lurking for the future. If e.g. I had a browser extension to prefetch links (for faster navigation) the same issue would be present.

The problem is logging out on a GET request, as discussed in current top comment. It's just semantically incorrect and many tools will (correctly) assume GET requests don't have side-effects (in HTTP's terms, it's a safe method).

E.g. it's very easy to have a GET request cached by mistake (and I've seen some faulty proxies do that, completely ignoring the upstream cache-control).

This is not a problem on Next or its Link component. It's on op's code (and maybe auth0-nextjs allowing logout on GET requests).


So, in summary, because you added a whole bunch of stuff that didn't need to be there, it broke. Colour me surprised.

It sounds like this could be implemented almost completely without any of that, especially as you were using JavaScript for the data.

All I can hope is that you've learnt a lesson about unnecessary overcomplication.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: