How we bridged the gap between a VS Code extension, a Next.js frontend, and a Rust Axum backend — and the bizarre bugs we fought along the way.
TL;DR: Integrating three stacks (Rust backend, Node.js/Electron extension, Next.js dashboard) is where the real bugs hide — not in your logic, but at the seams between ecosystems. This post covers 4 production-grade bugs: a silent
fetchbody corruption, a literal Rust compiler crash, a UI state machine that silently erased user work, and a secure OAuth token relay across three services.
The Rust backend was bulletproof. The Next.js dashboard was polished. The VS Code extension felt completely native. We were on the home stretch.
Then came the final 10%.
If you've shipped anything real, you already know what this means. Not because the work is hard in the traditional sense — but because you've crossed out of any single ecosystem's comfort zone. You're no longer debugging your code. You're debugging the spaces between Rust, Node.js, and Next.js. The gaps nobody writes documentation for.
These are the bugs that live there.
🐛 Bug #1 — The Silent fetch Body Corruption
The Setup
The VS Code extension needed to upload cover images to our Rust Axum backend. Since VS Code extensions run in a Node.js environment, we reached for the native fetch API plus the form-data npm package — a completely standard, well-documented combo.
The code looked textbook-correct:
import FormData from 'form-data';
const formData = new FormData();
formData.append('file', buffer, { filename: 'cover.jpg' });
const response = await fetch('https://api.dotsuite.dev/v1/media/upload', {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
...formData.getHeaders() // Sets Content-Type + boundary
},
body: formData as any
});
The Symptom
The Rust backend threw this on every single attempt:
Multipart error: Error parsing multipart/form-data request
The Investigation
We spent hours auditing the Rust Axum Multipart extractor. We logged raw bytes. We checked content-type headers. Everything on the Rust side looked sane.
Then we flipped our perspective: what if the problem was in the request itself, not the parser?
Here's the key insight. There are two completely different things called "FormData":
Web FormData (window.FormData) |
form-data npm package |
|
|---|---|---|
| Lives in | Browser / Web APIs | Node.js ecosystem |
| Body type | Serializes itself natively | Returns a readable stream |
Works with native fetch? |
✅ Yes | ❌ No |
Node's native fetch was designed around the Web FormData interface. When you pass it a form-data stream, it doesn't know how to pipe the boundary markers into the body correctly. The headers said Content-Type: multipart/form-data; boundary=---12345 — but the body was malformed. The Rust parser saw garbage.
The Fix
We replaced native fetch with axios for this specific upload path. Axios understands how to serialize Node.js stream-based FormData objects properly:
import axios from 'axios';
const response = await axios.post(
'https://api.dotsuite.dev/v1/media/upload',
formData,
{
headers: {
'Authorization': `Bearer ${token}`,
...formData.getHeaders(),
},
maxBodyLength: Infinity, // No cap on upload size
}
);
Immediately, the Rust backend parsed the stream perfectly, validated the magic bytes, and forwarded the file to Cloudflare R2.
💡 Lesson: Native
fetchin Node.js is NOT a drop-in replacement fornode-fetchoraxioswhen dealing with npm'sform-datastreams. The Web API and the Node.js ecosystem have diverged here in a subtle, silent, and infuriating way.
💥 Bug #2 — We Crashed the Rust Compiler
The Setup
While refining the post scheduler, we hit an error that sends ice down the spine of any Rust developer. Not a runtime panic. Not a logic error.
The compiler itself crashed.
thread 'rustc' panicked at library/alloc/src/vec/mod.rs:2873:36:
slice index starts at 16 but ends at 15
error: the compiler unexpectedly panicked. this is a bug.
query stack during panic:
#0 [check_mod_deathness] checking deathness of variables in module `platforms`
This is called an ICE — Internal Compiler Error. It means rustc hit a state it was never supposed to reach. These are filed as bugs against the Rust project itself.
The Cause
The stack trace pointed to check_mod_deathness — the compiler pass that finds dead (unused) code to issue dead_code warnings.
We had this struct:
pub struct PublishResult {
pub platform: Platform,
pub post_url: Option<String>,
}
We were producing PublishResult values inside an iterator chain, but the post_url field was never actually consumed anywhere downstream. The dead-code analysis pass, when it encountered this specific pattern — an unused field inside a struct flowing through an iterator chain — hit an edge case and panicked internally.
The compiler wasn't wrong to complain. We were writing dead code. It just wasn't supposed to crash about it.
The Fix
The tempting shortcut was #[allow(dead_code)]. That silences the warning without fixing anything.
The right fix was to actually use the field. We updated the scheduler to log published URLs on success — which made post_url a live, consumed value:
// Actively consume post_url — turns dead code into observable telemetry
let published_urls: Vec<String> = results.iter()
.filter_map(|r| r.as_ref().ok())
.filter_map(|r| {
r.post_url
.as_ref()
.map(|url| format!("{:?}: {}", r.platform, url))
})
.collect();
if !published_urls.is_empty() {
tracing::info!(urls = ?published_urls, "✅ Post published successfully");
}
By consuming the field, the dead-code analyzer had nothing to flag. We bypassed the compiler bug entirely — and gained structured logs as a bonus.
💡 Lesson: An ICE is the compiler's way of saying "this code revealed a bug in me." But the underlying cause is almost always real dead code or a degenerate abstraction. Fix the dead code first —
#[allow(dead_code)]is a bandage, not a solution. You can also report the ICE to help the Rust team fix the compiler bug itself.
🗑️ Bug #3 — The "Success" That Wiped Everything
The Setup
With backend and network layers stable, we focused on UX polish inside the VS Code extension's webview. That's when users started reporting something maddening:
"I upload a cover image and my entire post — title, tags, content — disappears."
The Investigation
We traced it to a central MessageHandler inside the webview that listened for backend status events:
// ❌ The bug: generic "success" = nuclear reset
case 'status':
if (msg.type === 'success') {
resetAllComposers(); // Wipes title, tags, content, everything
}
The logic made sense in isolation: a success status meant a post had been published to the cloud, so clear the UI for the next post.
The problem: uploading a cover image also emitted a success status. The event system didn't distinguish between:
- ✅ "Image uploaded successfully" (informational — keep the form)
- ✅ "Post published successfully" (workflow complete — reset the form)
Both were { type: 'success' }. The UI couldn't tell them apart, so it did what it was told: wiped everything.
The Fix
We separated informational success from workflow completion at the message contract level:
// ✅ The fix: explicit, semantic events
// Generic status messages only control spinners and toast notifications
case 'status':
updateLoadingState(msg);
showToast(msg.message);
break;
// Dedicated events for actual workflow transitions
case 'shareComplete':
resetMainComposer();
break;
case 'blogShareComplete':
resetBlogComposer();
break;
The backend now emits shareComplete or blogShareComplete only when the final publishing transaction commits. Image uploads, connection checks, and other intermediate operations stay as status events.
💡 Lesson: Generic event types are a trap. When your event bus grows,
{ type: 'success' }is as meaningful as{ type: 'thing happened' }. Name events after what specifically occurred — not the sentiment of the outcome.imageUploadCompleteandpostPublishedare unambiguous.successis not.
🔐 Bug #4 — Bridging OAuth Across Three Services
The Problem
This wasn't a crash — it was a design gap. Our Next.js frontend handles OAuth handshakes with X, LinkedIn, and Dev.to. Our Rust backend needs those tokens to execute the scheduled posts. The VS Code extension needs to know which platforms are connected to render the right UI buttons.
Three services. One source of truth. Zero raw tokens exposed to the client.
The Architecture
We implemented a secure internal handshake with a strict data flow:
┌─────────────────────────────────────────────────────────────┐
│ │
│ 1. User connects LinkedIn via Next.js OAuth flow │
│ │
│ 2. Next.js → POST /v1/internal/credentials/sync (Rust) │
│ Body: { user_id, platform, encrypted_token } │
│ Auth: internal service secret (never client-exposed) │
│ │
│ 3. Rust stores encrypted token, bound to user_id │
│ │
│ 4. VS Code Extension → GET /v1/oauth/connections (Rust) │
│ Response: { linkedin: true, x: false, devto: true } │
│ ↑ │
│ Boolean map only — no tokens, ever │
│ │
└─────────────────────────────────────────────────────────────┘
Key security properties:
- Tokens never touch the VS Code client. The extension only receives a boolean map of connected platforms.
- The sync endpoint is internal-only. Protected by a service secret, not a user JWT.
- Encryption at rest. Tokens are encrypted before storage in Rust, so even a database breach doesn't expose raw credentials.
The VS Code extension UI reacts dynamically — scheduling buttons enable/disable based on the boolean map, with zero coupling to the underlying OAuth tokens:
// Extension side — clean, no tokens in sight
const connections = await api.get<ConnectionMap>('/v1/oauth/connections');
// { linkedin: true, x: false, devto: true }
setScheduleButtonEnabled('linkedin', connections.linkedin);
setScheduleButtonEnabled('x', connections.x);
setScheduleButtonEnabled('devto', connections.devto);
💡 Lesson: When bridging auth across services, define exactly what each service needs to know — and nothing more. The VS Code client doesn't need tokens; it needs a boolean. Expose the minimum necessary data at each boundary.
Wrapping Up: The Bugs Live at the Seams
The individual stacks were each fine. Rust was fast and correct. Next.js was clean. The VS Code extension behaved exactly as expected in isolation.
Every single bug in this post happened at a boundary:
- Node.js streams meeting a Rust multipart parser
- A compiler analysis pass encountering dead code in an iterator chain
- A generic UI event system conflating two different kinds of success
- Three services needing to share auth state without sharing secrets
Cross-stack integration isn't about making things work — it's about making sure the assumptions baked into each ecosystem don't silently contradict each other. They will. Treat every boundary as a potential failure point, test each one in isolation before connecting them, and when something breaks, look at the interface before blaming the implementation.
That's how DotSuite went from a collection of three isolated services to one coherent, production-ready system.
This concludes the DotSuite Backend Architecture series. If you found this useful, the previous parts cover the concurrent cloud scheduler, the zero-trust media upload pipeline, and the OAuth flow in detail. Happy shipping.
Top comments (0)