In this post, we’ll walk through our journey integrating the Sightengine image moderation API into a Node.js application. Along the way, we encountered and solved several challenges:
- Native
fetch
+form-data
incompatibility - DNS resolution failures in Node.js
- Blob vs. stream limitations
- Final streaming solution with
form‑data
+node‑fetch
1. The Initial Approach: Native fetch
+ form-data
We began by trying to mirror our working curl -F
snippet in pure Node.js using the built‑in fetch
(Undici) and the popular form-data
package:
import FormData from 'form-data';
import fs from 'fs';
const data = new FormData();
data.append('media', fs.createReadStream('./image2.webp'));
…
await fetch('https://api.sightengine.com/1.0/check.json', {
method: 'POST',
headers: data.getHeaders(),
body: data
});
However, this led to a ConnectTimeoutError
and eventually a DNS ENOTFOUND
—even though curl
worked perfectly.
2. Hunting Down DNS Failures
A quick nslookup api.sightengine.com
returned:
;; Got SERVFAIL reply from 127.0.0.53
server can’t find api.sightengine.com: SERVFAIL
Our system was using systemd-resolved
on 127.0.0.53
, which silently failed. The solution was to bypass this resolver and point to public DNS:
# /etc/systemd/resolved.conf
[Resolve]
DNS=8.8.8.8 1.1.1.1
FallbackDNS=1.1.1.1
After sudo systemctl restart systemd-resolved
, both nslookup
and our Node.js code could resolve the API host.
3. Native FormData
and Blob vs. Streams
Next, wanting to stick with pure native APIs, we tried using:
import { Blob } from 'buffer';
const stream = fs.createReadStream('image.jpg');
const blob = new Blob([stream], { type: 'image/jpeg' });
formData.append('media', blob, 'image.jpg');
But Undici’s FormData.append
still only accepts string
, Buffer
, or real Blob
contents—not raw Node streams—so the API returned a “could not read image” error.
4. The Final, Battle‑Tested Solution
We ultimately opted for the most reliable approach in Node.js land:
- Install
npm install form-data node-fetch
- Code
import fs from 'fs';
import fetch from 'node-fetch';
import FormData from 'form-data';
const form = new FormData();
form.append('media', fs.createReadStream('./image.jpg'));
form.append('models', 'nudity-2.1,weapon,offensive-2.0,gore-2.0,self-harm');
form.append('api_user', 'your-api-user');
form.append('api_secret', 'your-api-secret');
const res = await fetch('https://api.sightengine.com/1.0/check.json', {
method: 'POST',
body: form,
headers: form.getHeaders()
});
const result = await res.json();
console.log(result);
This mirrors curl -F
exactly, streams the file (no big memory hit), and produces valid multipart requests.
Conclusion
Integrating file uploads in Node.js can feel straightforward—until you hit native vs. library quirks and platform DNS oddities. By:
- Fixing your DNS resolver
- Recognizing Undici’s
FormData
limitations - Falling back to
form-data
+node-fetch
you’ll have a rock‑solid, streaming multipart setup that behaves just like curl -F
.
Top comments (10)
Great breakdown! Switching from native fetch to form-data in Node.js can really smooth out multipart uploads, especially when dealing with image moderation APIs. And that sneaky DNS resolution issue? Classic Node headache — glad you tackled it head-on. These kinds of insights save devs a ton of debugging time!
Thanks! 🙌 Yeah, switching to form-data made a huge difference
Great insights! I’ve faced similar issues with Undici’s FormData while managing file streams — it’s really frustrating when it fails without any notice during an API upload.
If you’re debugging this: make sure your multipart payload is actually streaming, and don’t underestimate the quirks of DNS resolvers on Linux (I’ve been caught out by 127.0.0.53 more times than I can count).
A good fallback is using node-fetch with form-data. I’m curious if you’ve tried axios too? It’s great with streams, but it does come with a bit of overhead.
I’ve mostly stuck with node-fetch + form-data as a reliable combo, but haven’t seriously tried axios for stream uploads yet.
pretty cool seeing someone stick it out til stuff finally works - you think it’s patience or just stubbornness that actually makes you push through all the tech headaches?
Honestly, probably a mix of both
The DNS resolution issue via systemd-resolved was especially tricky—thanks for highlighting that fix. Also appreciate the clarity on Undici's Blob vs. stream limitations; it's not well-documented and can waste hours. The final solution with form-data and node-fetch is battle-tested and solid.
Glad the fix helped!
Super helpful breakdown - I’ve run into almost the exact same fetch vs. form-data headaches.
Did you see any signs that native APIs might eventually make this less painful?
Appreciate it! Yeah, you're definitely not alone—handling fetch and form-data in Node can feel way rougher than it should be. As for native APIs, there's some movement: newer Node versions are gradually aligning more with web standards (like the experimental FormData and Blob in undici), but there’s still a lot of edge-case pain, especially with streaming.