Skip to content

fix(article): reject cover uploads carrying more than one file - #216

Merged
aquie00t merged 1 commit into
mainfrom
fix/article-cover-single-file
Aug 25, 2026
Merged

fix(article): reject cover uploads carrying more than one file#216
aquie00t merged 1 commit into
mainfrom
fix/article-cover-single-file

Conversation

@aquie00t

Copy link
Copy Markdown
Collaborator

What does this PR do?

Makes the one-cover-per-article rule explicit at the upload endpoint.

The limit already existed in the data model — articles has a single cover_image_key column, so a second cover cannot be attached. What was missing was the endpoint saying so. request.file() keeps the first part and discards the rest without a word, so a client that posted three images received a 200 and no way to tell which one was stored.

Measured, before

request response
1 file 200
3 files 200 — first kept, rest silently dropped
5 files 200 — same

After

request response
1 file 200
2 files 400 MediaLimitExceededError
5 files 400 MediaLimitExceededError
0 files 400 NoMediaProvidedError

How

The handler iterates the multipart parts rather than taking the first: it consumes the first file, then checks whether another follows. The check runs before anything reaches storage, so a rejected request leaves no orphaned object in the bucket.

Note the ordering matters — the first part's buffer has to be consumed before the iterator can advance, which is why the size and magic-byte validation still happens on exactly the file that would have been stored.

Not included, deliberately

I had proposed a separate POST /articles/media endpoint for images embedded in the markdown body. Dropped at the author's direction: external ![alt](url) URLs already work, and the body is never rendered server-side, so there is no server-side sink. Cover images stay the only upload path.

Verification

lint, format:check, build clean; 763 unit tests pass.

Two new e2e cases assert that two files and three files are both refused. Verified end to end against the dev database with the table above.


Type of Change

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Chore

Checklist

  • My branch follows the naming convention (feature/, fix/, chore/, docs/)
  • My commits follow Conventional Commits
  • I have tested my changes locally
  • I have not introduced any breaking changes
  • I have updated relevant documentation if needed

🤖 Generated with Claude Code

An article holds exactly one cover: there is a single cover_image_key column,
so the limit already existed in the data model. The upload endpoint did not
honour it honestly, though - request.file() keeps the first part and discards
the rest without a word, so a client that posted three images got a 200 and no
way to tell which one was stored.

Measured before the change:

  1 file  -> 200
  3 files -> 200   (first kept, rest silently dropped)
  5 files -> 200   (same)

The handler now iterates the parts, consumes the first, and refuses if a
second one follows. The check runs before anything reaches storage, so a
rejected request leaves no orphaned object in the bucket.

After:

  1 file  -> 200
  2 files -> 400 MediaLimitExceededError
  5 files -> 400 MediaLimitExceededError
  0 files -> 400 NoMediaProvidedError

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@aquie00t
aquie00t merged commit 2eaeb15 into main Aug 25, 2026
9 of 10 checks passed
@aquie00t
aquie00t deleted the fix/article-cover-single-file branch August 25, 2026 05:50
@aquie00t

Copy link
Copy Markdown
Collaborator Author

CI caught a broken test file on the first push — fixed and re-pushed.

The multipart helper I added ended up with literal newlines where CRLF escapes belonged, so Buffer.from("<newline>") was an unterminated string. The file failed to parse, vitest collected (0 test) from it, and CI reported a failing suite rather than failing assertions.

Worth naming why nothing local caught it, because it is the same gap I flagged on #215:

  • pnpm build type-checks tsconfig.build.json, which excludes tests/
  • pnpm test:unit does not run the e2e suite
  • eslint ignores tests/** entirely

So a test file can be syntactically broken and every local check still passes. This is the second time it has bitten in this session.

Verified before re-pushing this time, two ways:

  • tsc -p tsconfig.json --noEmit (the base config does include tests) — no errors for this file
  • esbuild tests/e2e/article/upload-cover.test.ts — exits 0 and both new test names appear in the output, which is the same transform vitest performs

A typecheck script wired into Quality Checks would close this properly. It needs its own PR first, because the base config currently surfaces 7 files with pre-existing type errors that would turn CI red on day one.

aquie00t added a commit that referenced this pull request Aug 25, 2026
…217)

#216 merged while its CI was red, so main carries a test file that cannot be
parsed: the multipart helper has literal newlines where CRLF escapes belong,
which leaves Buffer.from("<newline>") as an unterminated string. vitest
collects zero tests from the file and reports the suite as failing, so the
E2E job on main is currently red.

This is the fix that was already on the PR branch but had not reached the PR
when it was merged.

Verified two ways before pushing, since none of the local checks cover test
files - build type-checks tsconfig.build.json which excludes tests/, test:unit
does not run e2e, and eslint ignores tests/**:

  tsc -p tsconfig.json --noEmit   no errors for this file
  esbuild <file>                  exits 0, both new test names in the output

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
github-actions Bot pushed a commit that referenced this pull request Aug 25, 2026
## [1.7.1](v1.7.0...v1.7.1) (2026-08-25)

### Bug Fixes

* **article:** reject cover uploads carrying more than one file ([#216](#216)) ([2eaeb15](2eaeb15))
* **test:** repair the unterminated string in the cover upload suite ([#217](#217)) ([e4655ab](e4655ab)), closes [#216](#216)
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.7.1 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant