<?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: Onuralp Akca</title>
    <description>The latest articles on DEV Community by Onuralp Akca (@scripthpp).</description>
    <link>https://dev.to/scripthpp</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%2F4032685%2Fcf13dfe2-0562-4b9a-bcb6-5ff8347d4b9f.jpg</url>
      <title>DEV Community: Onuralp Akca</title>
      <link>https://dev.to/scripthpp</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/scripthpp"/>
    <language>en</language>
    <item>
      <title>Two bugs I only found by running my Rust sync daemon against real infrastructure</title>
      <dc:creator>Onuralp Akca</dc:creator>
      <pubDate>Thu, 16 Jul 2026 19:43:55 +0000</pubDate>
      <link>https://dev.to/scripthpp/two-bugs-i-only-found-by-running-my-rust-sync-daemon-against-real-infrastructure-4278</link>
      <guid>https://dev.to/scripthpp/two-bugs-i-only-found-by-running-my-rust-sync-daemon-against-real-infrastructure-4278</guid>
      <description>&lt;p&gt;I built &lt;a href="https://github.com/Script-hpp/notionless" rel="noopener noreferrer"&gt;notionless&lt;/a&gt;, a small Rust daemon that mirrors a Notion database into Paperless-ngx (a self-hosted document archive) as Markdown, so notes end up in the same full-text-searchable place as everything else you own. &lt;code&gt;cargo build&lt;/code&gt;, &lt;code&gt;cargo test&lt;/code&gt;, and &lt;code&gt;cargo clippy&lt;/code&gt; were all green the whole time. Two real bugs only showed up once I pointed it at my actual, internet-facing Paperless instance, and both taught me something about &lt;code&gt;reqwest&lt;/code&gt; and API design that unit tests couldn't have caught.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: reqwest silently drops the Authorization header on a scheme-change redirect
&lt;/h2&gt;

&lt;p&gt;Paperless-ngx paginates results with a &lt;code&gt;next&lt;/code&gt; field containing the full URL for the next page. Behind a reverse proxy without &lt;code&gt;X-Forwarded-Proto&lt;/code&gt; set, Paperless believes its own scheme is &lt;code&gt;http://&lt;/code&gt;, even though the instance is actually served over &lt;code&gt;https://&lt;/code&gt;. My client followed that &lt;code&gt;next&lt;/code&gt; URL as-is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
    &lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// url came straight from Paperless' `next` field&lt;/span&gt;
    &lt;span class="nf"&gt;.header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Authorization"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Token {}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;.send&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;
    &lt;span class="py"&gt;.json&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;PaperlessResponse&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="k"&gt;.await&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First page: fine. Second page onward: a 401, and a JSON decode error, because the 401 body didn't match my expected response struct. The proxy was redirecting &lt;code&gt;http://&lt;/code&gt; to &lt;code&gt;https://&lt;/code&gt; transparently, and &lt;code&gt;reqwest&lt;/code&gt; follows redirects by default, but a scheme change counts as a different origin, so it strips the &lt;code&gt;Authorization&lt;/code&gt; header on the hop. The request that reached Paperless carried no token at all.&lt;/p&gt;

&lt;p&gt;The fix wasn't "always upgrade to https", since that breaks the equally common case of Paperless running on a plain &lt;code&gt;http://&lt;/code&gt; LAN address with no proxy in front of it at all. Instead, I only take the path and query from &lt;code&gt;next&lt;/code&gt; and rebuild the URL against the &lt;em&gt;configured&lt;/em&gt; base, never trusting the scheme Paperless reports back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;normalize_next_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;String&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;path_and_query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="nf"&gt;.find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"://"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scheme_end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;after_scheme&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;scheme_end&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
            &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="n"&gt;after_scheme&lt;/span&gt;&lt;span class="nf"&gt;.find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sc"&gt;'/'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="nf"&gt;Some&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path_start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;after_scheme&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;path_start&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s"&gt;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="nb"&gt;None&lt;/span&gt; &lt;span class="k"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;next&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="nd"&gt;format!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"{}{}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path_and_query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A LAN instance stays &lt;code&gt;http&lt;/code&gt;, a proxied instance stays &lt;code&gt;https&lt;/code&gt;, and the token survives every page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: "no error" isn't the same as "it worked"
&lt;/h2&gt;

&lt;p&gt;Paperless processes uploads asynchronously: &lt;code&gt;POST /api/documents/post_document/&lt;/code&gt; returns a 200 with a task ID immediately, before the document is actually validated or stored. Whether it was accepted, rejected as a duplicate, or failed outright only shows up later in a separate task-status endpoint. My first version treated the 200 as success and moved on, so uploads that were silently rejected (e.g. because the exact same file already existed) just vanished with no error and no log line.&lt;/p&gt;

&lt;p&gt;The fix was to poll the task endpoint until it resolves, and to make the resulting &lt;code&gt;enum&lt;/code&gt; do the explaining instead of a &lt;code&gt;Result&amp;lt;(), E&amp;gt;&lt;/code&gt; that can't distinguish "succeeded" from "the server said no, but the HTTP request itself didn't fail":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;enum&lt;/span&gt; &lt;span class="n"&gt;TaskResult&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Success&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="nf"&gt;Failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// Paperless' plain-text reason, e.g. a duplicate message&lt;/span&gt;
    &lt;span class="n"&gt;Timeout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That, in turn, surfaced a second-order problem: Paperless detects duplicates by hashing the raw file bytes, so re-syncing a page whose Paperless document already existed (e.g. from before I started using this tool) would get rejected forever, every cycle, since the content was already there under an unlinked document. Now, on a duplicate rejection, the daemon parses the existing document's ID out of Paperless' error message and links it to the Notion page instead of re-uploading, a five-minute bug turned into a small feature.&lt;/p&gt;




&lt;p&gt;Repo: &lt;a href="https://github.com/Script-hpp/notionless" rel="noopener noreferrer"&gt;https://github.com/Script-hpp/notionless&lt;/a&gt; (Rust, MIT licensed, Dockerfile included).Happy to talk through any of the design choices in the comments.&lt;br&gt;
&lt;em&gt;This article was drafted by Claude (Anthropic's coding assistant) based on my&lt;br&gt;
description of the bugs and fixes; I reviewed it and verified every technical claim&lt;br&gt;
against my own live Paperless/Notion instance and the project's source.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>rust</category>
      <category>showdev</category>
      <category>opensource</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
