postiz-x-posting-images-success

How I Fixed the Postiz X Media Upload Bug: A First-Hand Account

September 24, 20254 min read

The Problem That Drove Me Crazy

I was using Postiz to schedule posts to X (Twitter) when I discovered a frustrating issue. My text posts to X worked perfectly, but any post with images or videos would show as "published" in the Postiz calendar while never actually appearing on X.

After testing both text-only and media posts, I confirmed that text posts worked but media posts failed silently. This told me it wasn't an authentication issue - something was specifically wrong with how Postiz handled media uploads to X.

Getting My Hands Dirty with Container Access

I accessed the Postiz container to investigate:

bash

docker exec -it postiz-service sh

Then I checked the worker logs to see what was happening:

bash

pm2 logs workers --lines 50

Buried in the logs, I found the smoking gun:

$.media.media_ids[0]: object found, string expected

This error from X's API told me exactly what was wrong - Postiz was sending media IDs as objects when X expected strings.

Hunting Down the Bug in the Code

I needed to find where Postiz handles media uploads for X. I searched for the relevant files:

bash

find /app -name "*.ts" | xargs grep -l "media_ids"

This pointed me to the X provider file. I examined the source code:

bash

cat /app/libraries/nestjs-libraries/src/integrations/social/x.provider.ts

Looking at the media upload logic around lines 314-355, I could see the problem. The code assumed client.v1.uploadMedia() would always return a string, but in newer versions of the twitter-api-v2 library, it returns an object like {media_id_string: "123", media_id: 123}.

I confirmed this was a version issue by checking:

bash

cat /app/package.json | grep twitter-api-v2

Postiz was using "twitter-api-v2": "^1.24.0", which spans versions where this API response format changed.

Making the Fix

I backed up the original file first:

bash

cp /app/libraries/nestjs-libraries/src/integrations/social/x.provider.ts /app/libraries/nestjs-libraries/src/integrations/social/x.provider.ts.backup

Instead of manually editing the file, I used sed to make the precise change:

bash

sed -i 's/acc\[val\.postId\]\.push(val\.id);/acc[val.postId].push(typeof val.id === "object" ? val.id.media_id_string || String(val.id.media_id) : val.id);/' /app/libraries/nestjs-libraries/src/integrations/social/x.provider.ts

This command replaced the problematic line that just pushed val.id with code that checks if it's an object and extracts the string value appropriately.

I verified the change was applied:

bash

grep -n "typeof val.id" /app/libraries/nestjs-libraries/src/integrations/social/x.provider.ts

The Build Challenge

When I tried to rebuild the application, I hit a memory issue:

bash

npm run build:backend
FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory

I solved this by increasing Node.js memory allocation:

bash

export NODE_OPTIONS="--max-old-space-size=4096"rm -rf node_modules/.cachenpm run build:backend

This time the build completed successfully.

Restarting and Testing

I restarted the workers to load the new code:

bash

pm2 restart workerspm2 status

Initially, I saw some Redis connection errors in the logs, but I realized these were old timestamps from before the restart. To confirm Redis was working, I tested connectivity from within the container:

bash

docker exec -it postiz-service sh -c "nc -zv redis-cache 6379"

Getting redis-cache (172.20.0.3:6379) open confirmed the connection was working.

The Moment of Truth

I monitored the logs in real-time while testing:

bash

docker exec -it postiz-service sh -c "pm2 logs workers --follow"

Then I created a test post in Postiz with an image attachment and scheduled it to X.

It worked. The post appeared on X with the media properly attached, and the dreaded $.media.media_ids[0]: object found, string expected error was gone.

Success



What I Learned

The fix came down to one line of defensive programming that handles both the old string format and new object format returned by the twitter-api-v2 library:

typescript

acc[val.postId].push(typeof val.id === "object" ? val.id.media_id_string || String(val.id.media_id) : val.id);

This approach ensures backward compatibility while fixing the immediate issue.

The most frustrating part was that this was a silent failure - posts showed as "published" in Postiz but never appeared on X. The only way to catch it was by digging into the container logs and finding that specific API error message.

Using sed for the fix was cleaner than opening an editor, especially since I knew exactly what needed to change. The precise regex replacement ensured I modified only the problematic line without risking any other changes to the codebase.

Why This Happened

This bug occurred because Postiz uses a version range (^1.24.0) for the twitter-api-v2 dependency, and somewhere within that range, the library changed how uploadMedia() returns media IDs. The code worked fine with older versions that returned strings, but broke when the library started returning objects.

The fix I implemented handles both formats, so the issue won't resurface even if the dependency gets updated again. It's a simple but effective solution that turns a breaking change into a non-issue through defensive programming.

About Regard: Building Freedom Through Shared Knowledge

Regard launched Real & Works after grinding through the chaos of content marketing, wearing every hat in the book—writer, WordPress coder, systems architect, graphic designer, video editor, and analytics guru. The hustle was relentless, but the burnout was inevitable. Running a one-person show while competing with studios flush with staff wasn’t just tough—it was draining every ounce of time and resources he had.

Armed with a deep background in programming and systems design, Regard decided to break the cycle. He built automated content pipelines, starting with a streamlined YouTube shorts video workflow that hums along via self-hosted setups, powered by service APIs for inference, composition, and posting. It’s lean, it’s mean, and it’s entirely under his control—no subscriptions, no middlemen, just pure, efficient creation on his own terms.

Now, Regard’s mission isn’t about landing clients—it’s about spreading knowledge to set creators free. He builds in public, sharing every step, stumble, and success, from the code to the crashes. His goal? To show that anyone with enough grit and guidance can build their own automated systems, right on their own servers, using APIs to make it happen. Follow his journey, grab the lessons from his wins and losses, and take charge of your own creative freedom.

Regard Vermeulen

About Regard: Building Freedom Through Shared Knowledge Regard launched Real & Works after grinding through the chaos of content marketing, wearing every hat in the book—writer, WordPress coder, systems architect, graphic designer, video editor, and analytics guru. The hustle was relentless, but the burnout was inevitable. Running a one-person show while competing with studios flush with staff wasn’t just tough—it was draining every ounce of time and resources he had. Armed with a deep background in programming and systems design, Regard decided to break the cycle. He built automated content pipelines, starting with a streamlined YouTube shorts video workflow that hums along via self-hosted setups, powered by service APIs for inference, composition, and posting. It’s lean, it’s mean, and it’s entirely under his control—no subscriptions, no middlemen, just pure, efficient creation on his own terms. Now, Regard’s mission isn’t about landing clients—it’s about spreading knowledge to set creators free. He builds in public, sharing every step, stumble, and success, from the code to the crashes. His goal? To show that anyone with enough grit and guidance can build their own automated systems, right on their own servers, using APIs to make it happen. Follow his journey, grab the lessons from his wins and losses, and take charge of your own creative freedom.

LinkedIn logo icon
Back to Blog