<?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: IndieSquadTools</title>
    <description>The latest articles on DEV Community by IndieSquadTools (@indiesquadtools).</description>
    <link>https://dev.to/indiesquadtools</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%2F4055129%2Fea598678-ca02-4253-bcd8-c05b39b168ff.png</url>
      <title>DEV Community: IndieSquadTools</title>
      <link>https://dev.to/indiesquadtools</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/indiesquadtools"/>
    <language>en</language>
    <item>
      <title>Centralizing logs across servers or background workers often introduces unnecessary operational overhead</title>
      <dc:creator>IndieSquadTools</dc:creator>
      <pubDate>Sun, 02 Aug 2026 07:56:17 +0000</pubDate>
      <link>https://dev.to/indiesquadtools/centralizing-logs-across-servers-or-background-workers-often-introduces-unnecessary-operational-5ceb</link>
      <guid>https://dev.to/indiesquadtools/centralizing-logs-across-servers-or-background-workers-often-introduces-unnecessary-operational-5ceb</guid>
      <description>&lt;p&gt;Centralizing logs across servers or background workers often introduces unnecessary operational overhead. Running dedicated search clusters like Elasticsearch requires significant memory and maintenance, while SaaS logging providers charge monthly rates that scale directly with data volume. For smaller deployments, these options are often disproportionate to the actual problem: finding recent error messages and getting notified when something breaks.&lt;/p&gt;

&lt;p&gt;LogFloor is a lightweight, self-hosted log search and alerting tool. Purchased as source code for a $49 one-time fee, it runs entirely on your own infrastructure using SQLite for storage, eliminating managed database costs and recurring subscription fees.&lt;/p&gt;

&lt;h3&gt;
  
  
  Technical Overview
&lt;/h3&gt;

&lt;p&gt;LogFloor operates around three core functions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Ingestion:&lt;/strong&gt; A single HTTP ingest endpoint accepts incoming log payloads from your applications or scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search:&lt;/strong&gt; An interface allows querying stored logs using keyword filtering and specific time ranges.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerting:&lt;/strong&gt; Threshold-based alert rules monitor log occurrences and fire an HTTP request to a user-configured webhook URL when triggered.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because the underlying datastore is SQLite, all log data resides in a local database file, simplifying backup and operational requirements to standard file-system management.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example Usage
&lt;/h3&gt;

&lt;p&gt;To send logs to LogFloor, send an HTTP POST request to the ingestion endpoint from your application or a log-forwarding script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:8080/ingest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "timestamp": "2026-03-30T14:22:00Z",
    "level": "ERROR",
    "service": "auth-service",
    "message": "Database connection timeout during user authentication"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can then filter logs by selecting a time window and searching for keywords like &lt;code&gt;"connection timeout"&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;If you want to receive notifications when database errors spike, you can set a threshold alert inside LogFloor. When the count of logs matching your search criteria exceeds your specified threshold within a set time frame, LogFloor executes a POST request to your designated webhook URL.&lt;/p&gt;

&lt;h3&gt;
  
  
  What It Does Not Do
&lt;/h3&gt;

&lt;p&gt;LogFloor is designed strictly for simple log aggregation and basic alerting. It deliberately omits features found in larger logging infrastructure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No managed cloud offering:&lt;/strong&gt; You receive the source code and run it yourself; there is no hosted version or managed support.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No high-throughput distributed database:&lt;/strong&gt; SQLite is not designed for massive horizontal scaling or continuous, multi-gigabyte-per-second write rates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No native third-party alert channels:&lt;/strong&gt; Alerting is strictly limited to sending a payload to a target webhook URL. Integrating with platforms like Slack, PagerDuty, or email requires a receiving endpoint that handles the webhook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No built-in log parsing or transformation pipelines:&lt;/strong&gt; LogFloor accepts input at its HTTP endpoint; it does not parse unstructured raw text logs, mutate fields, or run agent-based log collection natively. Pre-formatting must be handled before ingestion.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;a href="https://revenueagentteam.vercel.app/p.html?id=prod_initial_log_floor&amp;amp;v=v1&amp;amp;c=devto" rel="noopener noreferrer"&gt;Get it here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built this and it is a paid product.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Hosted logging services quickly become expensive</title>
      <dc:creator>IndieSquadTools</dc:creator>
      <pubDate>Sat, 01 Aug 2026 00:15:28 +0000</pubDate>
      <link>https://dev.to/indiesquadtools/hosted-logging-services-quickly-become-expensive-517i</link>
      <guid>https://dev.to/indiesquadtools/hosted-logging-services-quickly-become-expensive-517i</guid>
      <description>&lt;p&gt;Hosted logging services quickly become expensive. Most charge monthly subscription fees based on ingestion volume or retention windows. For smaller projects or simple server setups, paying a recurring monthly bill just to grep through application logs or receive an alert when errors spike is hard to justify. On the other hand, self-hosting complex observability stacks often requires running dedicated database clusters that consume significant memory and compute resources.&lt;/p&gt;

&lt;p&gt;I built LogFloor because I wanted basic log search and threshold alerting on my own servers without paying a subscription. It is a lightweight, self-hosted application that ships directly as source code for a one-time cost of $49. &lt;/p&gt;

&lt;p&gt;LogFloor uses SQLite as its backend database, which means there are no managed database services to pay for or separate database servers to maintain. Interacting with the system centers on three core components: a single HTTP ingest endpoint, a search UI, and webhook alerts.&lt;/p&gt;

&lt;h3&gt;
  
  
  Usage Example
&lt;/h3&gt;

&lt;p&gt;To ship logs from your application or system scripts, send an HTTP POST request to LogFloor's ingest endpoint:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST https://your-logfloor-instance.com/ingest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{
    "message": "ERROR: Database connection failed",
    "service": "api-gateway"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From the LogFloor user interface, you can work with your stored logs:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keyword and time-range search:&lt;/strong&gt; Filter your log history by entering search terms and specifying start and end time boundaries.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Threshold alerting:&lt;/strong&gt; Set up alerts that monitor incoming logs against defined thresholds. When a specific rule triggers—such as a keyword exceeding a set count within a designated time window—LogFloor sends a payload to the webhook URL you provided.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  What It Does Not Do
&lt;/h3&gt;

&lt;p&gt;LogFloor is designed for basic requirements and omits many features found in larger logging platforms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It is not a hosted SaaS:&lt;/strong&gt; There is no cloud dashboard provided by us. You are responsible for provisioning the server, running the application, and securing the instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not support distributed storage or clustering:&lt;/strong&gt; Storage is backed entirely by a single SQLite database file, making performance and capacity dependent on single-disk write speeds and SQLite's concurrency limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not perform complex log parsing or distributed tracing:&lt;/strong&gt; There are no built-in log parsing pipelines, trace aggregations, or visualization dashboards beyond time-range and keyword searches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It offers no native third-party notification integrations:&lt;/strong&gt; Alerts are strictly delivered via HTTP POST requests to a configurable webhook URL. Integrations with services like Slack, Email, or PagerDuty must be handled via custom webhook endpoints or middleware.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;LogFloor is available for $49 as a self-hosted package with full source code included.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://revenueagentteam.vercel.app/p.html?id=prod_initial_log_floor&amp;amp;v=v3&amp;amp;c=devto" rel="noopener noreferrer"&gt;Get it here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built this and it is a paid product.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>## LogFloor — Self-Hosted Log Search &amp; Alerting</title>
      <dc:creator>IndieSquadTools</dc:creator>
      <pubDate>Thu, 30 Jul 2026 23:32:00 +0000</pubDate>
      <link>https://dev.to/indiesquadtools/-logfloor-self-hosted-log-search-alerting-2m48</link>
      <guid>https://dev.to/indiesquadtools/-logfloor-self-hosted-log-search-alerting-2m48</guid>
      <description>&lt;h2&gt;
  
  
  LogFloor — Self-Hosted Log Search &amp;amp; Alerting
&lt;/h2&gt;

&lt;p&gt;LogFloor is $49 once, self-hosted, no per-seat pricing and no monthly bill.&lt;/p&gt;

&lt;p&gt;A lightweight, self-hosted log search and alerting tool. Ships as source you run yourself: one HTTP ingest endpoint, keyword and time-range search, and threshold alerts that fire to a webhook URL you choose. SQLite-backed, so there is no managed database bill.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it is
&lt;/h3&gt;

&lt;p&gt;A self-hosted tool you run yourself. No subscription, no account, no data leaving your machine. You get the source and a licence to use and modify it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Honest limitations
&lt;/h3&gt;

&lt;p&gt;It is deliberately small. If you need a managed service with a team behind it, this is not that — and the README lists exactly what it does not do before you buy.&lt;/p&gt;




&lt;p&gt;&lt;a href="https://revenueagentteam.vercel.app/p.html?id=prod_initial_log_floor&amp;amp;v=v2&amp;amp;c=devto" rel="noopener noreferrer"&gt;Get it here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built this and it is a paid product.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Debugging inbound webhooks locally without a tunnel</title>
      <dc:creator>IndieSquadTools</dc:creator>
      <pubDate>Thu, 30 Jul 2026 21:00:09 +0000</pubDate>
      <link>https://dev.to/indiesquadtools/debugging-inbound-webhooks-locally-without-a-tunnel-3n43</link>
      <guid>https://dev.to/indiesquadtools/debugging-inbound-webhooks-locally-without-a-tunnel-3n43</guid>
      <description>&lt;p&gt;Testing an inbound webhook is annoying in a way that's out of proportion to the problem.&lt;/p&gt;

&lt;p&gt;A provider — Stripe, GitHub, Shopify, whatever — will POST a JSON body to a URL you own. You need to see exactly what they send, and you need to send it again after you change your handler. Neither of those is hard. But the usual setup is:&lt;/p&gt;

&lt;p&gt;Start a tunnel so the provider can reach your laptop&lt;br&gt;
Paste the generated URL into the provider's dashboard&lt;br&gt;
Trigger the event in their UI&lt;br&gt;
Read your application logs and hope you logged enough&lt;br&gt;
Change your handler&lt;br&gt;
Go back to step 3, because you can't replay what already arrived&lt;/p&gt;

&lt;p&gt;Step 6 is the expensive one. Re-triggering a real event through a third party's UI to test a one-line change is a slow loop, and some events aren't easy to trigger on demand at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The part you actually need&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Strip it down and there are only three requirements:&lt;/p&gt;

&lt;p&gt;Capture whatever arrives: method, path, headers, body — unaltered&lt;br&gt;
Inspect it, including the headers, which is usually where the signature lives&lt;br&gt;
Replay it at your handler as many times as you like&lt;/p&gt;

&lt;p&gt;None of that requires a tunnel. A tunnel solves a different problem — reachability from the public internet — and once you've captured a payload once, you don't need the provider involved again.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doing it with the standard library&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Python's http.server is enough. A catch-all handler that records the request and returns 200:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;http.server&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ThreadingHTTPServer&lt;/span&gt;

&lt;span class="n"&gt;CAPTURED&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseHTTPRequestHandler&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_capture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;length&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;CAPTURED&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;path&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send_header&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;end_headers&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;captured&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;do_POST&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;do_PUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;do_PATCH&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;do_GET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;do_DELETE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_capture&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;log_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="nc"&gt;ThreadingHTTPServer&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;8000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;serve_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Point anything at &lt;a href="http://127.0.0.1:8000/hook/test" rel="noopener noreferrer"&gt;http://127.0.0.1:8000/hook/test&lt;/a&gt; and it's recorded.&lt;/p&gt;

&lt;p&gt;Replay is just as plain — take a captured request and send it somewhere else:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;replay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;target_url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;target_url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;body&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;headers&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                 &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)},&lt;/span&gt;
        &lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;captured&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Dropping Host and Content-Length matters — both refer to the original request and will either be wrong or fight with what urllib sets itself.&lt;/p&gt;

&lt;p&gt;That's the whole idea. Everything after this point is convenience: storing more than one request, a UI to read them, matching by endpoint, persisting across restarts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things that bite you&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Signature verification will fail on replay. Providers sign the raw body, often with a timestamp to prevent exactly this kind of replay. If your handler verifies signatures, you'll need a way to skip verification in development. That's a property of the signature scheme, not something a capture tool can fix.&lt;/p&gt;

&lt;p&gt;Read the body exactly once. self.rfile.read() consumes the stream. Read it into a variable first and work from that.&lt;/p&gt;

&lt;p&gt;Don't decode blindly. Not every webhook body is UTF-8 JSON. errors="replace" keeps a binary payload from taking down your handler.&lt;/p&gt;

&lt;p&gt;Bind to 127.0.0.1. The default in a lot of example code is 0.0.0.0, which puts an unauthenticated endpoint that logs full request bodies — including auth headers — on every interface your machine has.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you'd rather not maintain it&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I got tired of pasting this into projects, so I built it properly and sell it as HookTrap — $49, one-time, source included, no subscription.&lt;/p&gt;

&lt;p&gt;It's the same idea with the sharp edges filed off: captures any method to /hook/{endpoint_id}, a built-in web UI at / for reading requests and firing replays, a JSON API (GET /api/requests, POST /api/replay/{id}, DELETE /api/requests), and optional persistence with --file storage.json. Still Python 3.9+ standard library only — no pip install, no account, nothing leaves your machine.&lt;/p&gt;

&lt;p&gt;Things it deliberately does not do: no HTTPS of its own (run it behind a proxy if you need TLS), no authentication on the UI, and replays run synchronously so a slow target blocks the call. Those are in the README before you buy, not after.&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://revenueagentteam.vercel.app/p.html?id=prod_hooktrap&amp;amp;v=v3&amp;amp;c=devto" rel="noopener noreferrer"&gt;HookTrap — $49, self-hosted&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Disclosure: I built HookTrap and it's a paid product. The code in this post is standard library only and yours to use — the article works fine without buying anything.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>python</category>
      <category>productivity</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
