DEV Community

Just a Side Project
Just a Side Project

Posted on Originally published at justasideproject.blogspot.com

dev.to Rejected My Cross-Post With a Cryptic Error, and the Cause Was a Picture

The Blogger post went out fine, like it always does. The dev.to cross-post that's supposed to follow it a few seconds later didn't. What I got back instead was {"error":"Invalid markdown detected!","status":422} -- no line number, no hint about which part of the "markdown" was invalid, just a flat rejection. The strange part was that the exact same script had successfully cross-posted a dozen times before. Nothing about the pipeline had changed. Only the post had.

Ruling out the usual suspects

My first assumption was a broken quote or an unescaped character somewhere in the post text -- the kind of thing that trips up a naive markdown parser. I went through the HTML looking for stray backticks, unmatched asterisks, anything that could read as a formatting directive gone wrong. Nothing stood out. The post was written the same way as every other one: a handful of <p> and <h3> tags, one <pre><code> block. dev.to's API had swallowed posts with code blocks before without complaint.

What was actually different about this post, and the thing I almost didn't think to check, was that it had a diagram in it -- a small flow chart I'd generated with a Python/Pillow script and embedded directly as a base64 <img> tag, the same way I do for any post where a picture genuinely helps. Every other post in the cross-posting pipeline up to that point happened to be text-only, so this was the first time that code path had actually been exercised end to end.

Confirming it with the ugliest possible test

Rather than guess, I wrote a five-line script that posted the exact same payload directly against dev.to's API outside of my normal pipeline, so I could read the raw response instead of whatever my own error handling was hiding. Same 422, same message. Then I stripped just the <img> tag out of the HTML by hand and re-ran it. It went through immediately. That was the whole bug: a single embedded image, nothing else about the post, was enough to make dev.to's markdown processor give up entirely.

My best guess is that a base64 data URI is just an enormous single "word" from a markdown parser's point of view -- often tens of thousands of characters with no spaces -- and something about that length or character mix breaks whatever heuristics dev.to uses to detect front matter or malformed syntax. I don't have dev.to's parser source in front of me to confirm the exact mechanism, so I'll flag that part as a reasonable inference rather than a verified fact. What I do know for certain, because I reproduced it directly, is that the embedded image is what triggers the rejection.

The fix I didn't want to need

The annoying part is that removing the image isn't really a loss for the cross-post specifically. Every dev.to post already sets canonical_url back to the original Blogger post, precisely so readers and search engines both know where the "real" version lives. dev.to was never meant to carry every visual element -- it's a mirror with a pointer home. So the fix was to strip base64 images out only for the copy that goes to dev.to, and leave the Blogger original untouched:

import re

def _strip_base64_images(html: str) -> str:
    """dev.to's markdown parser rejects posts containing inline base64
    images with a generic 'Invalid markdown detected' error. The image
    still displays fine on the canonical (Blogger) post, so we only
    need to drop it from the copy sent to dev.to."""
    return re.sub(r'<img[^>]*src="​data:image[^"]*"[^>]*/?>', "", html)

One function, called right before the payload gets built, and every future post with a diagram in it cross-posts cleanly without me having to remember to do anything differently.

What made this one different from my other pipeline bugs

Most of the pipeline failures I've hit so far in this project have been about authentication -- the wrong Google account, an expired token, a permission that quietly vanished. This one was refreshing in a boring way: no account confusion, no silent failure, just a specific input shape that a specific API doesn't like, caught on the very first real attempt to send it. The error message was useless on its own, but the failure was 100% reproducible the moment I isolated the variable, which made it one of the faster bugs in this whole project to actually pin down once I stopped assuming it was the same class of problem as everything else that had gone wrong that week.

Related reading

Top comments (0)