Skip to content

Public API: DELETE /posts/{id} returns 500 for any id that does not resolve (unguarded posts[0] in getPost) #1998

Description

@BlakeB254

DELETE /api/public/v1/posts/{id} returns 500 instead of 404 whenever the id does not resolve to a live post — a nonexistent id, or one that has already been soft-deleted.

Cause

PostsService.getPost builds its return object guarding some fields against an empty result and not others:

const list = {
    group: posts?.[0]?.group,                            // guarded
    posts: await Promise.all((posts || []).map(...)),    // guarded
    integrationPicture: posts[0]?.integration?.picture,  // guarded
    integration: posts[0].integrationId,                 // NOT guarded  <-- throws
    settings: JSON.parse(posts[0].settings || '{}'),     // NOT guarded
};

Three fields in the same object literal use ?. / || [], so the empty case was clearly anticipated. The next two lines dereference posts[0] directly. When posts is empty, posts[0] is undefined and .integrationId throws.

The public API controller then propagates it:

async deletePost(org, id) {
    const getPostById = await this._postsService.getPost(org.id, id);
    return this._postsService.deletePost(org.id, getPostById.group);
}

Reproduction

Any id that does not resolve — it does not need to have existed:

curl -X DELETE -H "Authorization: $POSTIZ_API_KEY" \
  http://localhost:5000/api/public/v1/posts/thisidneverexisted

Observed: HTTP 500

ERROR [ExceptionsHandler] TypeError: Cannot read properties of undefined (reading 'integrationId')
    at PostsService.getPost (dist/libraries/nestjs-libraries/src/database/prisma/posts/posts.service.js:367:35)
    at async PublicIntegrationsController.deletePost (dist/apps/backend/src/public-api/routes/v1/public.integrations.controller.js:160:29)

Expected: 404 Not Found.

Why it is worth fixing beyond the status code

Deleting an already-deleted post hits this, so a client retrying a delete — after a timeout, or reconciling its own state against Postiz — gets a 500 that reads as a server fault rather than "already gone". That makes the operation effectively non-idempotent from the caller's side, and the error text points at integrationId, which sends you looking at integrations rather than at a missing post.

We hit it reconciling ~1,300 stale drafts and spent a while attributing the 500 to our own credentials and then to our database networking before reading the trace.

Suggested fix

Guard the two remaining fields the same way as their neighbours, and let the controller return 404 on an empty result:

integration: posts?.[0]?.integrationId,
settings: JSON.parse(posts?.[0]?.settings || '{}'),

Happy to open a PR if that shape is agreeable.

Environment

  • ghcr.io/gitroomhq/postiz-app:latest, image sha256:785f97312f66a347f…
  • self-hosted, Postgres + Temporal via docker compose

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions