<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nipun Arora</title>
    <description>The latest articles on DEV Community by Nipun Arora (@mrnipunarora).</description>
    <link>https://dev.to/mrnipunarora</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F229948%2F1e6e89ef-b7a4-4fe8-a09d-237fdf16c1b4.jpg</url>
      <title>DEV Community: Nipun Arora</title>
      <link>https://dev.to/mrnipunarora</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mrnipunarora"/>
    <language>en</language>
    <item>
      <title>Cloudflare's bot challenges silently break navigation POSTs (and how an Astro forms package survived them)</title>
      <dc:creator>Nipun Arora</dc:creator>
      <pubDate>Tue, 04 Aug 2026 16:34:59 +0000</pubDate>
      <link>https://dev.to/mrnipunarora/cloudflares-bot-challenges-silently-break-navigation-posts-and-how-an-astro-forms-package-3ak8</link>
      <guid>https://dev.to/mrnipunarora/cloudflares-bot-challenges-silently-break-navigation-posts-and-how-an-astro-forms-package-3ak8</guid>
      <description>

&lt;p&gt;Every payment attempt on the live site failed. Every test I ran passed.&lt;/p&gt;

&lt;p&gt;That sentence cost me a full day, ten hypotheses, and three patch releases. What I found at the bottom is something I now check on every site I touch, because the failure is invisible from every place developers normally look: Cloudflare's bot challenges can make navigation POST requests structurally impossible to complete, for real users only, while your tests sail through.&lt;/p&gt;

&lt;p&gt;Here is the whole arc, including the wrong turns, because the wrong turns are where the lesson lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;I maintain an open source forms package for Astro called &lt;a href="https://github.com/nipun-arora/cool-astro-forms" rel="noopener noreferrer"&gt;cool-astro-forms&lt;/a&gt;. One of its features is a payment page: a visitor lands on a branded page with an amount, passes a Turnstile check, clicks Pay, and the server creates a Stripe Checkout session and sends them there.&lt;/p&gt;

&lt;p&gt;The flow was a classic HTML form submit. Navigation POST to the create-session endpoint, server answers with a 303 redirect to Stripe. Boring, dependable, works without JavaScript. The kind of thing you write once and trust.&lt;/p&gt;

&lt;p&gt;It ran perfectly in development. It ran perfectly in my test browser. It ran perfectly under curl. Then the site owner tried to make a real payment in his own Chrome and got this: first click does nothing, second click shows a security check error.&lt;/p&gt;

&lt;p&gt;Normal window, private window, same result. Every attempt, all day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hypothesis graveyard
&lt;/h2&gt;

&lt;p&gt;I want to be honest about how long the wrong path was, because during it every single fix was real. They just were not the fix.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypothesis 1: expired Turnstile tokens.&lt;/strong&gt; Real bug. The token silently expires after about five minutes and the widget still shows success. A visitor who reads the page slowly submits a dead token. I shipped a recovery flow: dead token posts now bounce back to the page with the amount preserved and a fresh widget. Failures continued.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypothesis 2: the &lt;code&gt;remoteip&lt;/code&gt; parameter.&lt;/strong&gt; Real bug. I was passing the visitor's IP to Turnstile's verify endpoint, and dual-stack visitors can present different addresses to the page and the verify call. Verification failed for people whose networks behaved differently from mine, which is the purest form of works-from-my-machine. I dropped &lt;code&gt;remoteip&lt;/code&gt;. Failures continued.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hypotheses 3 through 9.&lt;/strong&gt; Consumed tokens. Browser extensions. Service workers. Cookies. Stale caches. Each one plausible, each one testable, each one innocent. Meanwhile the error codes I was surfacing kept saying &lt;code&gt;timeout-or-duplicate&lt;/code&gt;, which pointed back at tokens, which kept me circling the token theory long after it stopped explaining anything.&lt;/p&gt;

&lt;p&gt;The trap of a day like this: every fix improves the system, so it always feels like progress, and none of it touches the actual fault.&lt;/p&gt;

&lt;h2&gt;
  
  
  The breakthrough: instrument the browser that fails, not the one that works
&lt;/h2&gt;

&lt;p&gt;The turning point was giving up on reproducing the failure in my environment and instrumenting the failing browser itself, live, during a real attempt.&lt;/p&gt;

&lt;p&gt;What the network log showed changed everything. The first click's navigation POST to the create-session endpoint returned a 503 with the tab wedged mid-navigation. Then, from the same page, in the same browser session, with the same cookies, a &lt;code&gt;fetch()&lt;/code&gt; to the same endpoint went straight through and reached the application.&lt;/p&gt;

&lt;p&gt;Same origin. Same credentials. Same moment in time. The only difference was the transport.&lt;/p&gt;

&lt;p&gt;A third data point sealed it: a byte-identical request sent through curl, with the browser's user agent and &lt;code&gt;Sec-Fetch-Mode: navigate&lt;/code&gt; headers faithfully copied, passed in 1.5 seconds.&lt;/p&gt;

&lt;p&gt;So the request was fine, the browser was fine, and the server was fine. What was not fine was the combination: a &lt;strong&gt;navigation&lt;/strong&gt; POST, from a &lt;strong&gt;real browser&lt;/strong&gt;, whose TLS fingerprint the edge had decided not to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mechanism
&lt;/h2&gt;

&lt;p&gt;Cloudflare's bot defenses can challenge requests they find suspicious. For a normal GET navigation, that works: the visitor sees an interstitial, passes, and the request is replayed.&lt;/p&gt;

&lt;p&gt;A challenged navigation POST cannot work that way. The interstitial cannot replay the POST body. The request dies. The visitor sees either a wedge or an opaque 503, depending on timing. And because my page had already consumed the Turnstile token on that first doomed attempt, the second click carried a stale token and produced the perfectly misleading &lt;code&gt;timeout-or-duplicate&lt;/code&gt; error that kept me chasing token bugs.&lt;/p&gt;

&lt;p&gt;The cruelest part is the selection effect. The challenge fires based on client fingerprint trust. Automated test browsers and curl often present fingerprints the edge has no opinion about. The site owner's real, aging, extension-laden, cookie-rich Chrome was exactly the client that drew the challenge. My tests passed &lt;strong&gt;because&lt;/strong&gt; they were tests. His payments failed &lt;strong&gt;because&lt;/strong&gt; he was real.&lt;/p&gt;

&lt;p&gt;I never found this in any Cloudflare error log I had access to, and nothing in the application ever saw the request. The failure lived entirely in the space between the visitor and the origin.&lt;/p&gt;

&lt;h2&gt;
  
  
  The observation that cracked it open
&lt;/h2&gt;

&lt;p&gt;The site had previously run on WordPress with a paid payments plugin, and payments there never failed this way. Same domain, same Cloudflare zone, same protective settings.&lt;/p&gt;

&lt;p&gt;The owner asked the obvious question: why did WordPress work?&lt;/p&gt;

&lt;p&gt;The answer took one look at the plugin's source: it submits payments over AJAX. Not because its authors foresaw any of this, presumably, but because that is just how modern WordPress plugins are built. The transport was the entire difference. XHR and fetch requests do not get interstitials. There is no page to interrupt.&lt;/p&gt;

&lt;p&gt;The architecture was never the problem. The transport was.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: change the transport, not the security
&lt;/h2&gt;

&lt;p&gt;The patch that ended the saga did three things:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: native form submit (navigation POST, challengeable)&lt;/span&gt;
&lt;span class="c1"&gt;// After: fetch first, native submit kept as the no-JS fallback&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;FormData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;form&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the hop to Stripe is a plain GET&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;The payment page submits over &lt;code&gt;fetch&lt;/code&gt;. The endpoint recognizes fetch clients by the &lt;code&gt;Accept&lt;/code&gt; header and answers &lt;code&gt;200 {ok, url}&lt;/code&gt; with JSON instead of a 303.&lt;/li&gt;
&lt;li&gt;The hop to Stripe happens as &lt;code&gt;location.assign(url)&lt;/code&gt;, a plain GET navigation, which survives any challenge normally.&lt;/li&gt;
&lt;li&gt;The native form submit stays in place as the no-JS fallback, and failures render inline with the amount preserved and the edge's error code visible, so the next debugging day starts with data instead of guesses.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Just as important is what the fix did not do: no Cloudflare exception rules, no allowlists, no turning anything off. The zone's bot protections stayed fully active. Security settings that punish your checkout are not something to disable; they are something to route around with a transport that was never in their blast radius.&lt;/p&gt;

&lt;p&gt;The same night, the first click in the owner's own Chrome, the browser that had failed all day, rendered a Stripe checkout page.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"Works from my machine" has an evil twin.&lt;/strong&gt; Fails only from real machines. When your tests pass and real users fail, stop trying to reproduce it in your environment and instrument theirs. The failing browser is the only honest witness.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The edge is part of your application.&lt;/strong&gt; My code never saw the failing requests. No server log, no application error, nothing. If your mental model of the request path ends at your origin, an entire class of failures is invisible to you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Transport is an architectural decision.&lt;/strong&gt; A navigation POST and a fetch POST are not two flavors of the same thing. They pass through different machinery at the edge, they fail differently, and one of them cannot survive an interstitial. I picked navigation submits for their simplicity and paid for it. The fetch-first, GET-hop pattern is now baked into the package so nobody using it has to relearn this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Surface error codes all the way to the visitor's URL.&lt;/strong&gt; Half my wasted day came from errors that said nothing. The package now carries the edge's own error code into the recovery URL, which turns a visitor's "it did not work" into a diagnosis you can read off a screenshot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Real fixes can still be the wrong fix.&lt;/strong&gt; The token expiry bug was real. The dual-stack bug was real. Fixing them made the product better and the investigation longer, because visible progress is the best camouflage a root cause ever gets.&lt;/p&gt;




&lt;p&gt;The package that survived all this is &lt;a href="https://github.com/nipun-arora/cool-astro-forms" rel="noopener noreferrer"&gt;cool-astro-forms&lt;/a&gt;, MIT licensed, on &lt;a href="https://www.npmjs.com/package/cool-astro-forms" rel="noopener noreferrer"&gt;npm&lt;/a&gt;. The payment transport, the token-gated buttons, and the error surfacing described here all shipped as releases 0.1.5 through 0.1.10, each one from a finding on a live production site. If you run forms or payments behind Cloudflare, I hope this saves you the day it cost me.&lt;/p&gt;

</description>
      <category>astro</category>
      <category>cloudflare</category>
      <category>debugging</category>
      <category>webdev</category>
    </item>
    <item>
      <title>The Law of Demeter: Do Not Accept Candy From Strangers</title>
      <dc:creator>Nipun Arora</dc:creator>
      <pubDate>Sun, 15 Sep 2019 12:44:29 +0000</pubDate>
      <link>https://dev.to/mrnipunarora/the-law-of-demeter-do-not-accept-candy-from-strangers-3d0p</link>
      <guid>https://dev.to/mrnipunarora/the-law-of-demeter-do-not-accept-candy-from-strangers-3d0p</guid>
      <description>&lt;p&gt;The Law of Demeter is another of those laws that is so fascinating, and although not part of the &lt;a href="https://www.geeksprogramming.com/solid-oriented-design-design-principles/" rel="noopener noreferrer"&gt;5 basic SOLID principles&lt;/a&gt;, can be considered almost an annex to them.&lt;/p&gt;

&lt;p&gt;The idea of this law may seem quite simple and clear: "Do not accept candy from strangers." Its application is cumbersome, and that is why I wanted to explain it here today.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Problem
&lt;/h3&gt;

&lt;p&gt;Many times it happens that we have a class that in turn is made up of objects from other classes, and those, in turn, are from other objects. It is normal in object orientation, and it is fine. It seems to mean that we are creating our code from small classes that interact with each other.&lt;/p&gt;

&lt;p&gt;So far so good. The problem arises when one of the classes needs to use some of the objects a few levels below. We rolled the blanket to the head and we set about creating getters as if there were no tomorrow. That can be dangerous.&lt;/p&gt;

&lt;p&gt;The typical code that smells bad enough is something like this:&lt;/p&gt;

&lt;p&gt;getX().getY().getZ().doSomething() &lt;/p&gt;

&lt;p&gt;What is the problem here? Basically, we are coupling the code to the structure of the classes that make up the call chain.&lt;/p&gt;

&lt;p&gt;If that structure changes tomorrow in any of the classes involved, this code is affected. Is that bad? It depends, but generally yes, because this code is very prone to modifications: a single refactoring can result in a change in it.&lt;/p&gt;

&lt;p&gt;And that's a clear symptom that the code is not well done.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is The Law of Demeter?
&lt;/h3&gt;

&lt;p&gt;According to Peter Van Rooijen's explanation of The Law of Demeter:&lt;/p&gt;

&lt;p&gt;You can play by yourself.&lt;br&gt;
You can play with your own toys (but you can't take them apart).&lt;br&gt;
You can play with toys that were given to you.&lt;br&gt;
And you can play with toys you've made yourself.&lt;br&gt;
The Law of Demeter is basically a coupling detection mechanism, and it tells us that our object should not know the entrails of other objects with which it interacts. If we want you to do something, why not ask it directly instead of navigating its structure?&lt;/p&gt;

&lt;p&gt;The Law of Demeter says that an object should not know the entrails of other objects with which it interacts in favor of pedantry. I will say that the Law of Demeter is fulfilled when, having a function f of a class C , that function only calls functions of:&lt;/p&gt;

&lt;p&gt;C.&lt;br&gt;
An object created by  f.&lt;br&gt;
An object passed as an argument to  f.&lt;br&gt;
An object stored in C field.&lt;br&gt;
(Definition inspired by what you can find in Robert C. Martin's Clean Code).&lt;/p&gt;

&lt;p&gt;In my opinion, you do not have to be extremely strict with these rules, but you have to see when we are really taking advantage of the system to load the rules.&lt;/p&gt;

&lt;p&gt;If you are using an elegant class encapsulation, but then you fill it all up with getters, you are bursting that encapsulation.&lt;/p&gt;

&lt;p&gt;An easy way to detect when this law is being violated is that you end up with many concatenated calls, but it is not a 100% reliable reason (keep reading until the end and you will see why), and neither is it the only reason.&lt;/p&gt;

&lt;p&gt;This code could be written as follows, and would still be invalid:&lt;/p&gt;

&lt;p&gt;val x = getX()&lt;br&gt;
val y = getY()&lt;br&gt;
val z = getZ()&lt;br&gt;
z.doSomething()&lt;br&gt;
So basically, the idea is that if you're accessing the internal structure of another class to call your methods, you're probably violating the law.&lt;/p&gt;

&lt;h3&gt;
  
  
  How Do I Solve the Violations of The Law of Demeter?
&lt;/h3&gt;

&lt;p&gt;Sometimes I get the feeling that I only give bad news, but there is no single solution to this issue. For me, this law is more of a "code smells" alarm, which tells us that something is not very well built out there.&lt;/p&gt;

&lt;p&gt;If you need to access the structure of a class you depend on, it is probably because the responsibilities are not well distributed.&lt;/p&gt;

&lt;p&gt;This does not always apply and depends on the type of class we have. We can distinguish two types of different classes: objects and data structures. The first define behavior and the second store state. The former is normal to ask them to do things while the latter is more usual to ask them to give us things.&lt;/p&gt;

&lt;p&gt;That is why the application of this law to classes of the second type loses enough validity, and in general, we will not have to worry too much about it.&lt;/p&gt;

&lt;p&gt;Knowing that there is no single answer to the question of how to solve this, what options do you have?&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Add Extra Methods
&lt;/h4&gt;

&lt;p&gt;This is the most obvious option, and the one I recommend least. Instead of having several calls, you let each object make its corresponding sub-calls. You could have something like this:&lt;/p&gt;

&lt;p&gt;getX().doSomething() &lt;/p&gt;

&lt;p&gt;This in turn would call:&lt;/p&gt;

&lt;p&gt;getY().doSomething()  &lt;/p&gt;

&lt;p&gt;And so on. In some cases, it may be worth it, but normally what you are doing is hiding the problem, not solving it.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Architecture
&lt;/h4&gt;

&lt;p&gt;A good architecture plays a very important role in the decoupling of the different modules of the software, and therefore will greatly reduce the possibility of violating this law.&lt;/p&gt;

&lt;p&gt;Normally in an architecture, each layer will have a series of interfaces with which to communicate, and when thinking about those layers that will hide its implementation, we will be avoiding problems like the ones we have seen before.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Better Understand Your Domain
&lt;/h4&gt;

&lt;p&gt;The idea of domain, and in particular of Domain Driven Design, is quite simple. It comes to tell us that our problem can be based on some key concepts that define it, and having them very clear, the whole development process is greatly simplified. Not understanding our domain well can easily lead to not modeling the application well, and therefore many problems of this type will arise.&lt;/p&gt;

&lt;p&gt;In addition, Domain Driven Design puts a lot of emphasis on the different types of elements that can help us to model our software, so we will be much clearer and more detailed the topic we discussed before about the types of classes.&lt;/p&gt;

&lt;p&gt;Like everything in this world, it is interesting to understand the concept behind this law, but not to take it as something that we can not skip or ignore. It will have to be studied in each case if it makes sense and why.&lt;/p&gt;

&lt;p&gt;As a small rule, if you see that you need to access the structure of an object that contains logic, there is surely a more elegant way to do it.&lt;/p&gt;

&lt;p&gt;Always make the decisions thinking clearly whether is it likely to generate a coupling problem in future, but do not forget that over-thinking is as bad as the complete lack of it.&lt;/p&gt;

&lt;p&gt;Did you know this law? Do you know of any other system to detect and solve it? Tell us in the comments.&lt;/p&gt;

</description>
      <category>devops</category>
      <category>beginners</category>
    </item>
  </channel>
</rss>
