<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Debanshu Panigrahi]]></title><description><![CDATA[Documenting my technical journey through code, comprehensive documentation, and personal opinions on the evolving tech landscape. From deep-dive tutorials to ho]]></description><link>https://blog.debanshupanigrahi.dev</link><image><url>https://cdn.hashnode.com/uploads/logos/69cdf6922d4d5bd0f080b8d3/aafafe8c-6236-40e6-b83d-4b879b022fc9.png</url><title>Debanshu Panigrahi</title><link>https://blog.debanshupanigrahi.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Sun, 11 Oct 2026 23:27:30 GMT</lastBuildDate><atom:link href="https://blog.debanshupanigrahi.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Preview Your Project Inside VS Code with QuickServe (No More Context Switching)]]></title><description><![CDATA[When you're building a web page, even small changes usually follow the same cycle:

Save the file.

Switch to your browser.

Check the result.

Switch back to VS Code.

Repeat.


Those constant window]]></description><link>https://blog.debanshupanigrahi.dev/preview-your-project-inside-vs-code-with-quickserve-no-more-context-switching</link><guid isPermaLink="true">https://blog.debanshupanigrahi.dev/preview-your-project-inside-vs-code-with-quickserve-no-more-context-switching</guid><category><![CDATA[quickserve]]></category><category><![CDATA[VS Code]]></category><category><![CDATA[vscode extensions]]></category><category><![CDATA[VSCode Tips]]></category><category><![CDATA[Visual Studio Code]]></category><category><![CDATA[internal-browser]]></category><category><![CDATA[webview]]></category><category><![CDATA[HTML]]></category><category><![CDATA[md]]></category><category><![CDATA[live markdown preview]]></category><category><![CDATA[markdown]]></category><category><![CDATA[live server]]></category><category><![CDATA[local development]]></category><category><![CDATA[live reload testing]]></category><category><![CDATA[HMR]]></category><category><![CDATA[live-reload]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[Productivity]]></category><category><![CDATA[Frontend Development]]></category><category><![CDATA[Developer Tools]]></category><dc:creator><![CDATA[Debanshu Panigrahi]]></dc:creator><pubDate>Sun, 26 Jul 2026 08:05:32 GMT</pubDate><content:encoded><![CDATA[<p>When you're building a web page, even small changes usually follow the same cycle:</p>
<ol>
<li><p>Save the file.</p>
</li>
<li><p>Switch to your browser.</p>
</li>
<li><p>Check the result.</p>
</li>
<li><p>Switch back to VS Code.</p>
</li>
<li><p>Repeat.</p>
</li>
</ol>
<p>Those constant window switches may seem small, but over hundreds of edits they become a real productivity drain.</p>
<p>With <strong>QuickServe</strong>, you can preview your project <strong>directly inside VS Code</strong> using the built-in Internal Webview, allowing you to stay focused in a single workspace.</p>
<h2>Why use the Internal Browser?</h2>
<p>Instead of launching your default browser every time the server starts, QuickServe can open your project inside VS Code itself.</p>
<p><strong>Benefits include:</strong></p>
<ul>
<li><p><strong>Zero context switching</strong> between your editor and browser.</p>
</li>
<li><p>Keep your code and preview side-by-side.</p>
</li>
<li><p>Faster iteration while tweaking HTML and Markdown.</p>
</li>
<li><p>Continue using <strong>Live Reload</strong> and <strong>HMR</strong> just like an external browser.</p>
</li>
<li><p>Great for smaller screens or single-monitor setups.</p>
</li>
</ul>
<p>For many editing tasks, you never need to leave your editor.</p>
<blockquote>
<p><strong>Think of it like having your browser embedded next to your code.</strong> Edit, save, and instantly see the result without changing applications.</p>
</blockquote>
<h2>How to Enable It</h2>
<p>Open <strong>VS Code Settings</strong> and search for:</p>
<pre><code class="language-plaintext">QuickServe: Open With
</code></pre>
<p>Change the value from:</p>
<pre><code class="language-plaintext">browser
</code></pre>
<p>to:</p>
<pre><code class="language-plaintext">internalWebview
</code></pre>
<p>Or edit your <code>settings.json</code> directly:</p>
<pre><code class="language-json">{
    "quickserve.openWith": "internalWebview"
}
</code></pre>
<p>That's it.</p>
<p>The next time you start QuickServe, your project opens inside VS Code instead of your default browser.</p>
<h2>See It in Action</h2>
<img src="https://github.com/debanshup/quickserve-assets/blob/main/webview_output.gif?raw=true" alt="action" style="display:block;margin:0 auto" />

<h2>Requirements</h2>
<p>The Internal Webview requires:</p>
<ul>
<li><p><strong>VS Code v1.109 or later</strong></p>
</li>
<li><p><strong>Simple Browser</strong> enabled (bundled with VS Code)</p>
</li>
</ul>
<p>If these requirements aren't met, simply switch back to the default browser option.</p>
<h2>When Should You Use It?</h2>
<p>The Internal Browser is ideal for:</p>
<ul>
<li><p>Building static HTML pages</p>
</li>
<li><p>Writing Markdown documentation</p>
</li>
<li><p>Editing landing pages</p>
</li>
<li><p>Following tutorials</p>
</li>
<li><p>Rapid UI iteration</p>
</li>
<li><p>Testing HMR without leaving the editor</p>
</li>
</ul>
<p>If you're spending most of your time making small visual changes, this workflow keeps everything in one place.</p>
<h2>Choosing the Right Preview Mode</h2>
<p>QuickServe supports three preview modes:</p>
<table>
<thead>
<tr>
<th>Option</th>
<th>Opens In</th>
<th>Best For</th>
</tr>
</thead>
<tbody><tr>
<td><code>browser</code></td>
<td>Default browser</td>
<td>Full browser testing</td>
</tr>
<tr>
<td><code>internalWebview</code></td>
<td>VS Code</td>
<td><strong>Fast development with zero context switching</strong></td>
</tr>
<tr>
<td><code>none</code></td>
<td>Doesn't open automatically</td>
<td>Custom workflows</td>
</tr>
</tbody></table>
<h2>Conclusion</h2>
<p>Switching between applications dozens of times an hour interrupts your flow more than most developers realize.</p>
<p>By setting:</p>
<pre><code class="language-json">"quickserve.openWith": "internalWebview"
</code></pre>
<p>QuickServe keeps your editor and preview together, making development faster, smoother, and more focused.</p>
<p>Sometimes the biggest productivity improvement isn't adding new features—it's simply <strong>removing unnecessary context switches</strong>.</p>
]]></content:encoded></item><item><title><![CDATA[Part 2 — Why I Rebuilt mitm-core from the Ground Up]]></title><description><![CDATA[Introduction
In the previous article, I shared how a simple curiosity about HTTPS evolved into mitm-core and the challenges I encountered while building its first architecture.
The original design ach]]></description><link>https://blog.debanshupanigrahi.dev/part-2-why-i-rebuilt-mitm-core-from-the-ground-up</link><guid isPermaLink="true">https://blog.debanshupanigrahi.dev/part-2-why-i-rebuilt-mitm-core-from-the-ground-up</guid><category><![CDATA[networking]]></category><category><![CDATA[node js]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[proxy]]></category><category><![CDATA[proxy design pattern]]></category><category><![CDATA[mitm]]></category><category><![CDATA[mitm-core]]></category><dc:creator><![CDATA[Debanshu Panigrahi]]></dc:creator><pubDate>Sat, 25 Jul 2026 14:54:44 GMT</pubDate><content:encoded><![CDATA[<h2>Introduction</h2>
<p>In the previous article, I shared how a simple curiosity about HTTPS evolved into <strong>mitm-core</strong> and the challenges I encountered while building its first architecture.</p>
<p>The original design achieved its goal. It successfully intercepted HTTPS traffic, supported plugins, and provided a modular, event-driven pipeline. More importantly, it helped me understand what actually happens beneath every secure web request.</p>
<p>But as the project evolved, so did its requirements.</p>
<p>Features that once felt easy to add started interacting with each other in unexpected ways. State became harder to manage, lifecycles became increasingly complex, and solving one problem often introduced another somewhere else.</p>
<p>At that point, I had two choices.</p>
<p>I could continue patching the existing architecture, or I could step back, rethink the design, and build a foundation that would support the project's future instead of limiting it.</p>
<p>I chose the second option.</p>
<p>This article isn't about new features or performance improvements. It's about the architectural decisions behind the redesign—what changed, why it changed, and how those decisions shaped the next generation of <strong>mitm-core</strong>.</p>
<h2>Design Goals</h2>
<p>The redesign wasn't about making the proxy faster or adding more features.</p>
<p>It was about building an architecture that could evolve without becoming increasingly difficult to maintain.</p>
<p>Before writing a single line of code, I defined a few principles that every component of the new architecture should follow.</p>
<h3>Replace Context-Based Design with Scope-Based Design</h3>
<p>The original architecture stored almost everything inside a single ProxyContext.</p>
<p>As the project grew, this became increasingly difficult to reason about because different pieces of data had completely different lifetimes.</p>
<p>The redesign introduced scopes instead of a single shared context.</p>
<ul>
<li><p><strong>Session Scope</strong> — Data that lives for the lifetime of a client connection.</p>
</li>
<li><p><strong>Request Scope</strong> — Data created for a single request-response cycle.</p>
</li>
<li><p><strong>Lifecycle Scope</strong> — Temporary state that exists only while a specific pipeline phase is executing.</p>
</li>
</ul>
<p>By separating state according to its lifetime, each component owns only the data it is responsible for.</p>
<p>This separation also lays a much stronger foundation for handling protocols that support multiple requests over a single connection (request multiplexing) in the future.</p>
<h3>Make Resource Cleanup a First-Class Citizen</h3>
<p>One of the biggest sources of bugs in the original architecture was resource management.</p>
<p>Sockets, streams, and requests needed deterministic cleanup.</p>
<p>Instead of leaving cleanup to individual handlers, the new architecture centralizes it.</p>
<p>Every component follows the same cleanup strategy, ensuring streams are destroyed or gracefully closed once they're no longer needed.</p>
<p>This dramatically reduces socket leaks, dangling resources, and connection lifecycle issues.</p>
<h3>Make the Event System Fully Type-Safe</h3>
<p>Events are one of the primary extension points of the framework.</p>
<p>The new event manager is completely statically typed, allowing the compiler to verify event names and payloads before the code ever runs.</p>
<p>This removes an entire class of runtime mistakes while providing much better IDE support for plugin developers.</p>
<h3><strong>Introduce a Real Processing Pipeline</strong></h3>
<p>The original execution flow gradually became a collection of handlers.</p>
<p>The new design formalizes it into a <strong>Chain of Responsibility</strong> with explicit <strong>Phase Orchestration</strong>.</p>
<p>Each phase has a well-defined responsibility, executes in a predictable order, and remains independent from the others.</p>
<p>This makes the request lifecycle significantly easier to understand and extend.</p>
<h3><strong>Centralize Request State</strong></h3>
<p>Instead of every component maintaining its own temporary state, request-specific information is stored in a centralized state store.</p>
<p>This gives every phase controlled access to shared request data without tightly coupling different parts of the system.</p>
<h3><strong>Optimize Expensive Operations</strong></h3>
<p>Some operations—such as generating leaf certificates—are computationally expensive.</p>
<p>The redesigned architecture moves certificate generation into worker threads and introduces a layered caching strategy that combines in-memory caching with persistent filesystem storage.</p>
<p>The goal is simple: generate certificates only when necessary and reuse them whenever possible.</p>
<h3><strong>Build Reusable Infrastructure</strong></h3>
<p>Rather than solving isolated problems, the redesign introduces reusable infrastructure that can support future capabilities.</p>
<p>This includes components such as:</p>
<ul>
<li><p>A centralized response cache.</p>
</li>
<li><p>A configurable rule manager.</p>
</li>
<li><p>A dedicated plugin manager.</p>
</li>
<li><p>Shared state management.</p>
</li>
<li><p>A transport layer responsible for moving data through the proxy.</p>
</li>
</ul>
<p>Each of these systems exists independently of the request pipeline, making the overall architecture more modular and easier to extend.</p>
<p>These goals became the blueprint for the new architecture. Every major design decision that follows can be traced back to one or more of these principles.</p>
<h2>The Biggest Architectural Change</h2>
<p>The redesign wasn't about introducing new components.</p>
<p>It was about changing how I thought about the proxy itself.</p>
<p>In the original architecture, the <strong>connection</strong> was the center of everything. Most of the state, lifecycle, and processing revolved around a single context object that was passed through the pipeline.</p>
<p>The more features I added, the more responsibilities that object accumulated.</p>
<p>During the redesign, I asked myself a simple question:</p>
<blockquote>
<p><strong>What is the actual unit of work inside a proxy?</strong></p>
</blockquote>
<p>The answer wasn't "a connection."</p>
<p>A single connection contains multiple kinds of state, each with a completely different lifetime and responsibility.</p>
<p>Some data exists as long as the client stays connected.</p>
<p>Some exists only for a single request.</p>
<p>Some only exists while a particular pipeline phase is executing.</p>
<p>Trying to represent all of these with a single context object inevitably mixed responsibilities together.</p>
<p>The new architecture separates the proxy into independent scopes.</p>
<ul>
<li><p><strong>Session Scope</strong> manages everything related to the lifetime of a client connection.</p>
</li>
<li><p><strong>Request Scope</strong> owns everything required for a single request-response cycle.</p>
</li>
<li><p><strong>Lifecycle Scope</strong> stores temporary state that exists only while a pipeline phase is executing.</p>
</li>
</ul>
<p>Each scope has a clear owner, a clear lifetime, and a well-defined responsibility.</p>
<p>Instead of one object trying to represent everything, each layer now manages only the state it truly owns.</p>
<p>This change affected almost every part of the framework.</p>
<p>The pipeline became simpler.</p>
<p>Plugins became easier to reason about.</p>
<p>Cleanup became deterministic.</p>
<p>State propagation became explicit instead of implicit.</p>
<p>Most importantly, the architecture now reflects how the proxy actually behaves rather than forcing every piece of data into the same abstraction.</p>
<p>The complete architecture now looks like this:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69cdf6922d4d5bd0f080b8d3/fa3dc7b0-3237-4537-a5dc-54c31382eeb6.png" alt="" style="display:block;margin:0 auto" />

<p>After introducing scope-based state management, the remaining pieces naturally fell into place. The transport layer became centralized, the pipeline evolved into a proper phase orchestrator, resource management became predictable, and every subsystem gained a much clearer responsibility.</p>
<p>This wasn't just a refactor.</p>
<p>It was a shift from building <strong>features around an architecture</strong> to building <strong>an architecture that naturally supports new features</strong>.</p>
<h2>Separating State by Lifetime</h2>
<p>The core idea behind the redesign was surprisingly simple:</p>
<blockquote>
<p><strong>Not all data lives for the same amount of time, so it shouldn't live in the same object.</strong></p>
</blockquote>
<p>In the original architecture, almost every piece of information eventually found its way into a single context object. Connection details, request metadata, temporary pipeline state, routing information, and plugin data all coexisted in the same place.</p>
<p>This worked initially, but over time the boundaries between different responsibilities became unclear.</p>
<p>The redesign separates state according to its natural lifetime.</p>
<h3>Session Context</h3>
<p>A <strong>Session Context</strong> is created when a client establishes a connection with the proxy.</p>
<p>It contains information that remains valid throughout the lifetime of that connection, regardless of how many requests are processed.</p>
<p>Examples include:</p>
<ul>
<li><p>Connection identifiers</p>
</li>
<li><p>Underlying socket</p>
</li>
<li><p>Negotiated protocol</p>
</li>
<li><p>Connection-level errors</p>
</li>
<li><p>Custom TLS certificates</p>
</li>
</ul>
<p>Once the client disconnects, the session context is destroyed.</p>
<h3>Request Context</h3>
<p>Every incoming request creates its own <strong>Request Context</strong>.</p>
<p>Unlike the session context, this object exists only for the lifetime of a single request-response transaction.</p>
<p>It contains everything related to that request, including:</p>
<ul>
<li><p>Client request and response objects</p>
</li>
<li><p>Upstream request and response objects</p>
</li>
<li><p>Headers</p>
</li>
<li><p>Request and response bodies</p>
</li>
<li><p>Routing information</p>
</li>
<li><p>Status information</p>
</li>
</ul>
<p>Once the response has been sent back to the client, the request context is no longer needed.</p>
<p>This separation means multiple requests can safely exist within the same client session without interfering with one another.</p>
<h3>Request Lifecycle</h3>
<p>Some information doesn't belong to either the connection or the request.</p>
<p>Instead, it only exists while the proxy is actively processing a request through the pipeline.</p>
<p>For this, I introduced the <strong>Request Lifecycle</strong>.</p>
<p>Rather than storing operational state inside the request itself, the lifecycle tracks information such as:</p>
<ul>
<li><p>Current execution state</p>
</li>
<li><p>Phase transitions</p>
</li>
<li><p>Request hijacking</p>
</li>
<li><p>Timing metrics</p>
</li>
<li><p>Performance measurements</p>
</li>
</ul>
<p>This keeps execution state separate from business data while giving the pipeline complete control over how a request progresses.</p>
<h3>Why This Matters</h3>
<p>Separating state by lifetime solved several problems at once.</p>
<p>Responsibilities became much clearer because each context owned only the data relevant to its lifetime.</p>
<p>Cleanup also became far more predictable. When a request finishes, its request context and lifecycle can be discarded without affecting the surrounding session. Likewise, when a connection closes, the entire session context can be released safely.</p>
<p>Perhaps the biggest benefit, however, is that the architecture now mirrors how the proxy actually operates. Instead of forcing every piece of information into a single object, each layer has a clear owner, a clear purpose, and a well-defined lifecycle.</p>
<p>This separation became the foundation upon which the rest of the architecture was built.</p>
<h2>Centralized Transport Layer</h2>
<p>One of the biggest responsibilities of a proxy is moving data between two endpoints.</p>
<p>In the original architecture, each handler was responsible for creating or managing its own network connections. For example, the request handler initiated the upstream request, the handshake handler dealt with decrypted streams, and the response handler handled sending data back to the client.</p>
<p>While this worked, it also meant that transport logic was scattered throughout the pipeline.</p>
<p>The redesign introduces a dedicated <strong>Transport Layer</strong> whose sole responsibility is moving data between the client and the upstream server.</p>
<p>Instead of handlers creating sockets or managing streams directly, they simply delegate those responsibilities to the transport layer.</p>
<p>The transport layer is divided into three distinct responsibilities:</p>
<img src="https://cdn.hashnode.com/uploads/covers/69cdf6922d4d5bd0f080b8d3/a45a96c1-a92b-4dc2-abe8-f26dcc213e31.png" alt="" style="display:block;margin:0 auto" />

<ul>
<li><p><strong>Inbound</strong> — Receives and prepares incoming client traffic after the handshake phase.</p>
</li>
<li><p><strong>Upstream Initiator</strong> — Establishes and manages communication with the destination server when instructed by the request handler.</p>
</li>
<li><p><strong>Outbound</strong> — Returns the processed upstream response back to the client.</p>
</li>
</ul>
<p>This separation keeps networking concerns isolated from request processing.</p>
<p>Handlers now focus on <strong>what</strong> should happen, while the transport layer is responsible for <strong>how</strong> data moves through the proxy.</p>
<p>The result is a cleaner architecture with well-defined responsibilities, easier testing, and a single place to evolve transport-related behavior without affecting the rest of the pipeline.</p>
<h2>Pipeline Evolution</h2>
<p>One of the more subtle problems in the original architecture was pipeline ownership.</p>
<p>Although the proxy had clearly defined phases, there wasn't a single component responsible for orchestrating them.</p>
<p>Instead, each handler decided what should happen next by invoking the pipeline again. In other words, handlers weren't just processing their own phase—they were also responsible for deciding how execution continued.</p>
<p>This made the control flow harder to follow because the pipeline was effectively orchestrating itself.</p>
<p>The redesign establishes a much stricter boundary.</p>
<p>The <strong>Middleware</strong> becomes the single entry point into the execution pipeline.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69cdf6922d4d5bd0f080b8d3/07af2e30-c87b-4dcc-9e20-2789867e954e.png" alt="" style="display:block;margin:0 auto" />

<p>Once <a href="http://Pipeline.run"><code>Pipeline.run</code></a><code>()</code> begins, it becomes the sole owner of execution.</p>
<p>Each handler is responsible for only one thing:</p>
<ul>
<li><p>Execute its own phase.</p>
</li>
<li><p>Update the lifecycle if another phase should follow.</p>
</li>
<li><p>Return control to the pipeline.</p>
</li>
</ul>
<p>The pipeline then decides what happens next.</p>
<p>Instead of handlers recursively invoking the next stage, the pipeline continuously evaluates the current lifecycle state and orchestrates phase execution until the request completes or is intentionally terminated.</p>
<p>This seemingly small change has several advantages:</p>
<ul>
<li><p><strong>Handlers no longer control execution flow.</strong></p>
</li>
<li><p><strong>Pipeline orchestration exists in one place.</strong></p>
</li>
<li><p><strong>Phase transitions become predictable and easy to debug.</strong></p>
</li>
<li><p><strong>Aborting or hijacking a request becomes a first-class operation rather than a special case.</strong></p>
</li>
<li><p><strong>Error handling is centralized instead of duplicated across handlers.</strong></p>
</li>
</ul>
<p>By separating <strong>execution</strong> from <strong>orchestration</strong>, handlers became significantly smaller and easier to reason about, while the pipeline evolved into a true execution engine rather than just a collection of sequential function calls.</p>
<h2>Resource Management &amp; Cleanup</h2>
<p>One lesson I learned while building the first version of the proxy was that processing requests is only half the job.</p>
<p>The other half is cleaning everything up correctly.</p>
<p>A proxy sits between two endpoints and constantly manages sockets, streams, requests, and responses. If even one of these resources isn't released properly, problems begin to accumulate over time—dangling streams, half-open connections, memory growth, and difficult-to-debug networking issues.</p>
<p>In the original architecture, cleanup was largely the responsibility of individual handlers. Each handler had to remember which resources it created and when they should be released. This worked for simple cases, but it also meant cleanup logic became scattered throughout the codebase.</p>
<p>The redesign centralizes resource management into a dedicated cleanup utility.</p>
<p>Rather than each component implementing its own shutdown logic, handlers simply hand over the resources they own, and the cleanup layer safely closes or destroys them when their lifecycle ends.</p>
<pre><code class="language-typescript">ProxyUtils.cleanUp([
  clientSocket,
  upstreamSocket,
  requestStream,
  responseStream,
]);
</code></pre>
<p>This approach provides several advantages:</p>
<ul>
<li><p>A single, consistent strategy for releasing resources.</p>
</li>
<li><p>Less duplicated cleanup code across handlers.</p>
</li>
<li><p>Deterministic destruction of streams and sockets.</p>
</li>
<li><p>Reduced risk of dangling connections and resource leaks.</p>
</li>
<li><p>Simpler handlers that focus on request processing rather than lifecycle management.</p>
</li>
</ul>
<p>Most importantly, cleanup is now treated as part of the architecture instead of an implementation detail.</p>
<p>Every resource has a well-defined owner, and every owner has a clear responsibility for releasing it when its work is complete. That makes the entire request lifecycle more predictable and the proxy far more stable during long-running workloads.</p>
<h2><strong>Centralized State Store</strong></h2>
<p>As the proxy grew, different components needed a way to share operational state during a request's lifetime.</p>
<p>Rather than scattering temporary flags across multiple objects, the redesign introduces a <strong>centralized state store</strong> bound to each request lifecycle.</p>
<p>It provides a single location for tracking execution state, such as cache hits, completion status, and error conditions, while also allowing plugins and middleware to attach custom request-specific data.</p>
<p>The state store combines <strong>strongly typed built-in state</strong> with <strong>flexible custom entries</strong>, offering compile-time safety for core framework properties without restricting extensibility.</p>
<p>This gives every phase of the pipeline a consistent way to exchange information without tightly coupling components or polluting the request and session contexts.</p>
<h2><strong>Strongly Typed Event System</strong></h2>
<p>Events are one of the primary extension points in <strong>mitm-core</strong>, allowing plugins and internal components to react to different stages of the proxy lifecycle.</p>
<p>In the original architecture, the event system relied on loosely typed event names and payloads. While functional, it left room for runtime mistakes such as misspelled event names or incorrect payload structures.</p>
<p>The redesigned event manager is fully <strong>statically typed</strong>. Every event has a well-defined payload, allowing TypeScript to validate both event names and their associated data at compile time.</p>
<p>Beyond improving developer experience through better autocompletion and type inference, this approach eliminates an entire class of runtime errors and makes plugin development significantly safer.</p>
<p>As the framework continues to grow, the event system remains both extensible and reliable without sacrificing type safety.</p>
<h2><strong>Performance Optimizations</strong></h2>
<p>As the architecture matured, performance became just as important as correctness. Rather than optimizing prematurely, I focused on the parts of the proxy that were genuinely expensive or frequently repeated.</p>
<h3>Worker-thread certificate generation</h3>
<p>Generating leaf certificates is one of the most computationally expensive operations in a MITM proxy. Performing this work on the main event loop would introduce unnecessary latency and reduce the proxy's ability to handle concurrent requests.</p>
<p>To avoid blocking the event loop, certificate generation is delegated to a pool of worker threads using <strong>Piscina</strong>. This allows expensive cryptographic operations to run in parallel while the main thread remains responsive.</p>
<h3>Multi-layer certificate cache (LRU + filesystem)</h3>
<p>Even with worker threads, generating a certificate for every connection would be wasteful.</p>
<p>To eliminate redundant work, the proxy uses a two-level caching strategy:</p>
<ul>
<li><p><strong>In-memory LRU cache</strong> for frequently accessed certificates.</p>
</li>
<li><p><strong>Filesystem cache</strong> for long-term persistence across proxy restarts.</p>
</li>
</ul>
<p>This layered approach significantly reduces certificate generation overhead while maintaining fast lookup times for commonly requested domains.</p>
<h3>Response cache</h3>
<p>The redesigned architecture also introduces a dedicated response cache.</p>
<p>Rather than always forwarding identical requests to the upstream server, cacheable responses can be reused when appropriate, reducing network traffic and improving response times.</p>
<p>By treating caching as a dedicated subsystem instead of embedding it within request handlers, the proxy keeps its execution pipeline clean while making cache behavior easier to configure and extend.</p>
<p>Together, these optimizations reduce unnecessary computation, minimize repeated work, and keep the proxy responsive under sustained workloads without complicating the overall architecture.</p>
<h2><strong>Built for Extensibility</strong></h2>
<p>A proxy framework shouldn't require modifying its core every time new behavior is needed. One of the primary goals of the redesign was to make <strong>mitm-core</strong> extensible through dedicated infrastructure rather than hardcoded logic.</p>
<p>Two key components enable this: the <strong>Rule Manager</strong> and the <strong>Plugin Manager</strong>.</p>
<h3>Rule Manager</h3>
<p>Many proxy features depend on configurable rules—whether it's bypassing specific domains, blocking requests, or applying custom routing behavior.</p>
<p>The redesigned <strong>Rule Manager</strong> provides a generic framework for defining and managing these rule sets. Each rule engine owns its own parser and storage while sharing the same underlying infrastructure.</p>
<p>Rule files are automatically created if they don't exist, watched for changes, and reloaded using a debounced mechanism whenever they're modified. This allows configuration changes to take effect without restarting the proxy.</p>
<p>Because parsing logic is abstracted behind a simple interface, different rule formats can be implemented without changing the framework itself.</p>
<h3>Plugin Manager</h3>
<p>The <strong>Plugin Manager</strong> provides the primary extension mechanism for the framework.</p>
<p>Plugins subscribe to well-defined lifecycle events and receive strongly typed payloads corresponding to each stage of proxy execution. Rather than modifying the framework's internal components, developers can extend behavior by implementing small, focused plugins that react to specific events.</p>
<p>This event-driven model keeps the core architecture isolated while allowing new functionality—such as logging, monitoring, authentication, request inspection, or custom routing—to be added independently.</p>
<p>Together, the Rule Manager and Plugin Manager make <strong>mitm-core</strong> easier to customize without increasing coupling inside the framework. New behavior can be introduced by composing reusable components instead of modifying existing ones, keeping the architecture both flexible and maintainable as it grows.</p>
<h2>Conclusion</h2>
<p>Redesigning <strong>mitm-core</strong> wasn't about adding more features—it was about building a stronger foundation.</p>
<p>By separating state according to its lifetime, centralizing transport and resource management, formalizing pipeline orchestration, and introducing reusable infrastructure, the framework became significantly easier to understand, extend, and maintain.</p>
<p>Every architectural decision in this redesign was driven by the same principle: <strong>make the system reflect how a proxy actually works</strong>. The result is an architecture that is not only cleaner internally but also better prepared for future capabilities.</p>
<hr />
<h2>What's Next</h2>
<p>While this redesign establishes a solid foundation, there is still plenty of work ahead.</p>
<p>The next major milestone is expanding the proxy's protocol support, beginning with:</p>
<ul>
<li><p><strong>HTTP/2 interception</strong>, enabling inspection and modification of modern multiplexed HTTP traffic.</p>
</li>
<li><p><strong>WebSocket interception</strong>, allowing real-time bidirectional communication to be observed and manipulated through the same extensible pipeline.</p>
</li>
</ul>
<p>With the core architecture now in place, these capabilities can be introduced as natural extensions of the framework rather than requiring another fundamental redesign.</p>
]]></content:encoded></item><item><title><![CDATA[Building a MITM Proxy from Scratch: The Journey Behind mitm-core]]></title><description><![CDATA[Every networking project starts with "I'll just forward a request." A MITM proxy quickly teaches you that nothing on the Internet is ever that simple.
Introduction
I didn't start mitm-core with the go]]></description><link>https://blog.debanshupanigrahi.dev/building-a-mitm-proxy-from-scratch-the-journey-behind-mitm-core</link><guid isPermaLink="true">https://blog.debanshupanigrahi.dev/building-a-mitm-proxy-from-scratch-the-journey-behind-mitm-core</guid><category><![CDATA[networking]]></category><category><![CDATA[mitm-core]]></category><category><![CDATA[proxy]]></category><category><![CDATA[http]]></category><category><![CDATA[https]]></category><category><![CDATA[TLS]]></category><dc:creator><![CDATA[Debanshu Panigrahi]]></dc:creator><pubDate>Mon, 20 Jul 2026 17:31:28 GMT</pubDate><content:encoded><![CDATA[<h2>Every networking project starts with "I'll just forward a request." A MITM proxy quickly teaches you that nothing on the Internet is ever that simple.</h2>
<h3>Introduction</h3>
<p>I didn't start <a href="https://github.com/debanshup/mitm-core"><code>mitm-core</code></a> with the goal of building another proxy framework.</p>
<p>In fact I had a much simpler question:</p>
<blockquote>
<p><strong>How does an HTTPS request actually work under the hood?</strong></p>
</blockquote>
<p>Every day we type a URL into a browser, and within miliseconds a secure connection is established. We know the high-level concepts — TLS, certificates, encryption — but what really happens between clicking <strong>Enter</strong> and seeing a webpage?</p>
<p>I wanted to understand every step of that journey.</p>
<p>So I decided to build a simple MITM (Man-in-the-Middle) proxy from scratch. The idea wasn't to create a production-ready tool or compete with existing projects. It was purely an experiment—a way to learn by rebuilding the entire flow myself.</p>
<p>I expected to forward a few requests, generate some certificates, and call it a day.</p>
<p>I was completely wrong.</p>
<p>Every answer uncovered two more questions. A simple HTTP proxy became HTTPS interception. HTTPS interception led to certificate generation. Certificate generation led to TLS handshakes. Then came HTTP/2, ALPN negotiation, streaming, connection lifecycles, caching, plugins, and countless edge cases that browsers handle every second without us ever noticing.</p>
<p>What started as a weekend experiment gradually evolved into <strong>mitm-core</strong>—a modular, extensible MITM framework that taught me far more about networking than I ever expected.</p>
<p>In this article, I'll share that journey, the mistakes I made, the problems I encountered, and why they eventually led me to redesign the entire architecture from the ground up.</p>
<hr />
<h3>What is a MITM Proxy?</h3>
<p>Imagine you send a letter to your friend.</p>
<p>Normally, the journey looks like this:</p>
<pre><code class="language-plaintext">You ─────────────► Friend
</code></pre>
<p>Now imagine there's someone sitting in the middle.</p>
<p>Instead of sending the letter directly, it goes through them first.</p>
<pre><code class="language-plaintext">You ─────► Middle Person ─────► Friend
</code></pre>
<p>The middle person can:</p>
<ul>
<li><p>Read the letter</p>
</li>
<li><p>Modify its contents</p>
</li>
<li><p>Block it entirely</p>
</li>
<li><p>Or simply forward it without making any changes</p>
</li>
</ul>
<p>A <strong>Man-in-the-Middle (MITM) Proxy</strong> works in exactly the same way—but instead of letters, it sits between a client (such as your browser) and a server, intercepting network traffic.</p>
<pre><code class="language-plaintext">Browser ─────► MITM Proxy ─────► Website
</code></pre>
<p>From the browser's perspective, it's communicating directly with the website.</p>
<p>From the website's perspective, it's communicating directly with the browser.</p>
<p>The proxy quietly sits in the middle, forwarding data between both sides while having complete visibility into the communication.</p>
<p>This makes MITM proxies incredibly useful for tasks such as:</p>
<ul>
<li><p>Debugging HTTP requests and responses</p>
</li>
<li><p>Inspecting API traffic</p>
</li>
<li><p>Security testing</p>
</li>
<li><p>Performance analysis</p>
</li>
<li><p>Building developer tools</p>
</li>
</ul>
<p>Of course, intercepting encrypted HTTPS traffic isn't as straightforward as plain HTTP. Modern browsers expect to communicate securely with the server, so the proxy has to convince both sides that it's the legitimate endpoint.</p>
<p>That's where things start getting interesting—and much more complicated.</p>
<hr />
<h3>The rabbit hole</h3>
<p>At first, the plan seemed simple.</p>
<p>I thought I'd create a proxy that accepted an HTTP request, forwarded it to the destination server, received the response, and sent it back to the client.</p>
<p>Simple enough.</p>
<p>That illusion lasted only a few hours.</p>
<p>The moment I moved from HTTP to HTTPS, I discovered that I wasn't just forwarding requests anymore—I was participating in the entire communication process.</p>
<p>To inspect encrypted traffic, the proxy first has to establish a secure connection with the client. At the same time, it also needs another secure connection with the actual server. Somehow, it has to sit between both without either side realizing they're not talking directly to each other.</p>
<p>That was only the beginning.</p>
<p>One solved problem immediately revealed another:</p>
<ul>
<li><p>How do browsers decide whether to trust a certificate?</p>
</li>
<li><p>How are certificates generated on the fly for every domain?</p>
</li>
<li><p>What exactly happens during a TLS handshake?</p>
</li>
<li><p>How does the <code>CONNECT</code> method establish an HTTPS tunnel?</p>
</li>
<li><p>How does a proxy decrypt HTTPS traffic without the browser complaining?</p>
</li>
<li><p>How should requests and connections be managed throughout their lifecycle?</p>
</li>
<li><p>How can plugins intercept traffic without breaking the flow?</p>
</li>
<li><p>How do you handle errors, caching, and resource cleanup correctly?</p>
</li>
</ul>
<p>Every answer uncovered another layer of the networking stack.</p>
<p>What I initially thought was a single protocol turned out to be an ecosystem of protocols, handshakes, streams, state machines, and edge cases—all working together to make something as simple as loading a webpage feel instantaneous.</p>
<p>By this point, the project had stopped being "just a proxy."</p>
<p>It had become a deep dive into how modern web communication actually works. And before I realized it, I was no longer just learning HTTPS—I was designing the architecture needed to support it.</p>
<hr />
<h3>Designing the first architecture</h3>
<p>By this point, it was obvious that a simple "receive a request and forward it" approach wasn't going to scale.</p>
<p>Every connection had to go through multiple stages. A raw TCP connection wasn't the same as an HTTPS handshake, and an incoming request shouldn't be handled the same way as an outgoing response.</p>
<p>Instead of putting everything inside one giant function, I decided to split the proxy into a series of well-defined phases.</p>
<p>The idea was simple: each phase would have a single responsibility and execute in a predictable order.</p>
<img src="https://cdn.hashnode.com/uploads/covers/69cdf6922d4d5bd0f080b8d3/7a858ab5-378f-4b5e-99d9-8373f74768e9.png" alt="" style="display:block;margin:0 auto" />

<p>Each handler was responsible for only one stage of the connection lifecycle.</p>
<ul>
<li><p><strong>TCP Handler</strong> accepted and prepared new connections.</p>
</li>
<li><p><strong>Handshake Handler</strong> established secure communication for HTTPS traffic.</p>
</li>
<li><p><strong>Request Handler</strong> processed incoming requests before forwarding them upstream.</p>
</li>
<li><p><strong>Response Handler</strong> intercepted the upstream response before sending it back to the client.</p>
</li>
</ul>
<p>To keep the system extensible, the entire pipeline was driven by events. Plugins could subscribe to different stages without modifying the core implementation. This separation made the codebase easier to reason about and allowed new functionality to be added with minimal changes to the existing flow.</p>
<p>At the time, this architecture felt like the right balance between simplicity and flexibility. It was modular, easy to extend, and much cleaner than a monolithic request handler.</p>
<p>It also laid the foundation for everything that came next—even though, as I would later discover, it had limitations that only became visible as the project grew.</p>
<hr />
<h3>The engineering problems</h3>
<p>The first architecture worked.</p>
<p>It accepted connections, decrypted HTTPS traffic, forwarded requests, intercepted responses, and allowed plugins to hook into different stages of the lifecycle.</p>
<p>But as the project grew, the cracks started to appear.</p>
<p>Most of these weren't bugs—they were architectural limitations that became obvious only after adding more features.</p>
<ul>
<li><strong>One Context for Everything</strong></li>
</ul>
<p>The biggest issue was that everything lived inside a single <code>ProxyContext</code>.</p>
<p>Connection state, request state, certificates, routing information, temporary variables, plugin data—everything shared the same object.</p>
<p>Initially, this felt convenient.</p>
<p>Over time, it became increasingly difficult to determine which data belonged to the connection, which belonged to a single request, and which was only needed for a specific stage of the pipeline.</p>
<p>Plugins frequently mutated the same object, making state propagation harder to reason about and increasing coupling between different parts of the system.</p>
<ul>
<li><strong>Lifecycle Management Became Messy</strong></li>
</ul>
<p>As more features were added, managing the lifecycle of sockets and requests became increasingly complex.</p>
<p>There wasn't a clear separation between connection setup, request processing, response handling, and cleanup.</p>
<p>That led to several issues I spent a considerable amount of time debugging:</p>
<ul>
<li><p>Sockets remaining open longer than expected.</p>
</li>
<li><p><code>CLOSE_WAIT</code> connections caused by sockets not being cleaned up properly.</p>
</li>
<li><p>Premature socket termination.</p>
</li>
<li><p>Exceptions such as:</p>
</li>
</ul>
<pre><code class="language-plaintext">Error: Can't write to socket after response has been sent
</code></pre>
<p>None of these problems were particularly difficult on their own, but together they made the codebase harder to maintain.</p>
<ul>
<li><strong>The Event System Didn't Scale</strong></li>
</ul>
<p>The project relied heavily on events, but the original event manager wasn't statically typed.</p>
<p>As the number of events increased, it became easier to subscribe to incorrect event names, pass invalid payloads, or accidentally introduce inconsistencies that TypeScript couldn't detect.</p>
<p>The implementation also made listener management more difficult than it needed to be, increasing the risk of lingering listeners and unnecessary memory usage.</p>
<ul>
<li><strong>Resource Management</strong></li>
</ul>
<p>Network programming is fundamentally about managing resources.</p>
<p>Every accepted connection creates sockets.</p>
<p>Every HTTPS request introduces additional streams and state.</p>
<p>Every missed cleanup eventually consumes memory.</p>
<p>I learned this lesson the hard way.</p>
<p>A small oversight in socket cleanup could leave connections stuck in <code>CLOSE_WAIT</code>, gradually increasing resource usage until the proxy became unstable.</p>
<p>These weren't problems that could be solved with another <code>try...catch</code> block or a few extra conditionals.</p>
<p>They pointed to something deeper.</p>
<p>The architecture itself had reached a point where adding new functionality was becoming harder than it should have been.</p>
<p>And that's when I started asking a different question:</p>
<blockquote>
<p><strong>Instead of fixing individual problems, what if I redesigned the architecture so these problems became much harder to create in the first place?</strong></p>
</blockquote>
<hr />
<h3>The realization</h3>
<p>After fixing countless bugs, refactoring handlers, and improving different parts of the proxy, I noticed something interesting.</p>
<p>Most of the problems weren't caused by the individual components.</p>
<p>The certificate generation worked.</p>
<p>The request forwarding worked.</p>
<p>The plugin system worked.</p>
<p>The handlers worked.</p>
<p>Yet every new feature seemed to make the project a little harder to reason about.</p>
<p>I found myself spending more time managing interactions between components than actually building new functionality.</p>
<p>That was the moment I realized I was solving symptoms rather than the underlying problem.</p>
<p>The architecture that had helped me get the project off the ground was now limiting its growth.</p>
<p>Instead of asking,</p>
<blockquote>
<p><em>"How do I fix this bug?"</em></p>
</blockquote>
<p>I started asking,</p>
<blockquote>
<p><em>"Why is this bug possible in the first place?"</em></p>
</blockquote>
<p>That single question changed how I looked at the project.</p>
<p>Rather than continuing to patch the existing design, I decided to rethink the architecture from first principles.</p>
<p>If different kinds of state had different lifetimes, they shouldn't live in the same object.</p>
<p>If every stage had its own responsibility, it should also have its own lifecycle.</p>
<p>If resources needed explicit cleanup, the architecture should make cleanup a natural part of execution—not an afterthought.</p>
<p>That decision marked the end of the first generation of <strong>mitm-core</strong>.</p>
<p>The next version wasn't about adding more features.</p>
<p>It was about building a stronger foundation that would make future features easier to implement, easier to maintain, and far less error-prone.</p>
<p>In the next article, I'll walk through that redesign and explain how the new architecture emerged from these lessons.</p>
]]></content:encoded></item><item><title><![CDATA[Quickserve with HTTPS: The No-Fuss Guide]]></title><description><![CDATA[If you're using the QuickServe extension in VS Code and need a Secure Context for local testing (Service Workers, Clipboard API, etc.), you don't need to manually wrestle with OpenSSL via the CLI anym]]></description><link>https://blog.debanshupanigrahi.dev/quickserve-with-https-the-no-fuss-guide</link><guid isPermaLink="true">https://blog.debanshupanigrahi.dev/quickserve-with-https-the-no-fuss-guide</guid><category><![CDATA[VS Code]]></category><category><![CDATA[vscode extensions]]></category><category><![CDATA[VSCode Tips]]></category><category><![CDATA[https]]></category><category><![CDATA[openssl]]></category><category><![CDATA[networking]]></category><category><![CDATA[webdev]]></category><category><![CDATA[webdevelopment]]></category><category><![CDATA[mkcert]]></category><category><![CDATA[localhost]]></category><category><![CDATA[local development]]></category><category><![CDATA[frontend]]></category><category><![CDATA[DX]]></category><dc:creator><![CDATA[Debanshu Panigrahi]]></dc:creator><pubDate>Tue, 28 Apr 2026 15:28:45 GMT</pubDate><content:encoded><![CDATA[<p>If you're using the <a href="https://marketplace.visualstudio.com/items?itemName=debanshupanigrahi.quickserve">QuickServe</a> extension in VS Code and need a Secure Context for local testing (Service Workers, Clipboard API, etc.), you don't need to manually wrestle with OpenSSL via the CLI anymore.</p>
<p>QuickServe has a built-in <strong>Self-Healing Mode</strong> that handles certificate generation for you. You just need to tweak your <code>settings.json</code>.</p>
<p>Here is the exact setup.</p>
<h2>The Baseline Configuration</h2>
<p>Navigate to <strong>Settings &gt; Extensions &gt; User/Workspace &gt; QuickServe Configuration</strong> (or just open your <code>settings.json</code>).</p>
<p>You'll be targeting this JSON block:</p>
<pre><code class="language-json">"quickserve.https": {
    "enable": true,
    "certPath": "",
    "keyPath": ""
}
</code></pre>
<p>By setting <code>"enable": true</code> and leaving the paths empty, QuickServe takes over. What happens next depends on what is installed on your machine.</p>
<hr />
<h2>Method 1: The Self-Healing Fallback (With Browser Warning)</h2>
<p>If you don't have any local Certificate Authority tools installed, do absolutely nothing else.</p>
<p>With the empty paths above, QuickServe's Self-Healing Mode kicks in. It automatically generates a standard self-signed certificate in the background to serve your project over HTTPS.</p>
<p><strong>Result:</strong> Your browser will throw an <code>ERR_CERT_AUTHORITY_INVALID</code> warning because it's self-signed. Just click <strong>Advanced &gt; Proceed to localhost</strong> to bypass it, and you're good for quick testing.</p>
<hr />
<h2>Method 2: Trusted Local CA (No Browser Warning)</h2>
<p>Use <code>mkcert</code> to create a local Certificate Authority. This permanently prevents browser warnings and avoids strict cross-origin blocks.</p>
<h3>1. Install mkcert:</h3>
<pre><code class="language-shell"># macOS
brew install mkcert

# Windows (using Chocolatey)
choco install mkcert

# Linux (Ubuntu/Debian)
sudo apt install libnss3-tools mkcert
</code></pre>
<h3>2. Setup the local CA (Run once per machine):</h3>
<pre><code class="language-shell">mkcert -install
</code></pre>
<h3><strong>3. Let QuickServe Take Over:</strong></h3>
<p>With <code>mkcert</code> installed and the <code>certPath</code>/<code>keyPath</code> still empty in your <code>settings.json</code>, simply restart the QuickServe extension. The Self-Healing Mode will detect <code>mkcert</code> and automatically generate a fully trusted local certificate for you.</p>
<p><strong>Result:</strong> A smooth, trusted <a href="https://127.0.0.1"><code>https://127.0.0.1</code></a> connection. No red text, no warnings.</p>
<hr />
<h2>Bonus: The Manual Override</h2>
<p>If you already have your own custom OpenSSL certificates and want to bypass the Self-Healing Mode entirely, just provide the absolute paths in your <code>settings.json</code>:</p>
<p>JSON</p>
<pre><code class="language-json">"quickserve.https": {
    "enable": true,
    "certPath": "/absolute/path/to/cert.pem",
    "keyPath": "/absolute/path/to/key.pem"
}
</code></pre>
<p>Save the file, restart the extension, and QuickServe will use your specific files.</p>
]]></content:encoded></item></channel></rss>