Skip to content

wrangler: truncate Pages commit message at UTF-8 boundary#12378

Merged
vicb merged 7 commits intocloudflare:mainfrom
X6TXY:x6txy/fix-utf8-commit-message
Feb 5, 2026
Merged

wrangler: truncate Pages commit message at UTF-8 boundary#12378
vicb merged 7 commits intocloudflare:mainfrom
X6TXY:x6txy/fix-utf8-commit-message

Conversation

@X6TXY
Copy link
Copy Markdown
Contributor

@X6TXY X6TXY commented Feb 3, 2026

Fixes #11749

Safely truncate Cloudflare Pages commit messages at valid UTF-8 boundaries before sending them to the Pages deployments API.

Cloudflare Pages enforces a fixed byte limit (384 bytes) on git commit metadata. When a multi-line commit message containing multi-byte UTF-8 characters (e.g. Cyrillic, Japanese, emoji) is truncated mid-character on the server side, the resulting string becomes invalid UTF-8 and deployments fail with error 8000111.

This change ensures Wrangler never sends an invalid UTF-8 commit message by truncating at the nearest valid UTF-8 boundary on the client side.


What was changed

  • Added truncateUtf8Bytes() in packages/wrangler/src/api/pages/deploy.ts
    • Truncates UTF-8 strings to 384 bytes
    • Backs off to a valid UTF-8 boundary if truncation lands mid-sequence
  • Integrated truncation into the Pages deploy request so all commit messages are sanitized before being sent
  • Added comprehensive tests in packages/wrangler/src/__tests__/pages/utf8-truncation.test.ts
    • Cyrillic, Japanese, and emoji coverage
    • Boundary cases (exactly 384 bytes, continuation bytes at the cutoff)
    • Multi-line (5+ lines) messages reproducing the reported failure

Tests

  • Tests included/updated
  • Automated tests not possible - manual testing has been completed as follows:
  • Additional testing not necessary because:

Public documentation

Docs: Not required — this is an internal deployment-safety fix affecting only the Pages deploy API payload. No user-facing CLI or API changes.

@X6TXY X6TXY requested review from a team as code owners February 3, 2026 06:31
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Feb 3, 2026

🦋 Changeset detected

Latest commit: 465de13

The changes in this PR will be included in the next version bump.

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional flags.

Open in Devin Review


const MAX_COMMIT_MESSAGE_BYTES = 384;

export function truncateUtf8Bytes(str: string, maxBytes: number): string {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this should be moved to the workers-util package? What do others think?

At least we just push this util function toward the bottom of the file.

Copy link
Copy Markdown
Contributor Author

@X6TXY X6TXY Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point, i think it should be in workers-util

if (byteCount + charBytes > maxBytes) {
break;
}
result += char;
Copy link
Copy Markdown
Contributor

@vicb vicb Feb 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

perf: concatenating string is O(n^2) and 384^2 = 147k. It would be better to chars.push(char) and then return chars.join("")

return str.slice(...) and not pushing is also an option

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I’ll push an update using the bytes-first approach

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FYI, Gemini help me came up with:

export function truncateUtf8Bytes(str: string, maxBytes: number): string {
  const buf = Buffer.from(str);
  if (buf.length <= maxBytes) return str;

  let i = maxBytes;
  
  // Scan backwards from the limit (max 3 bytes for UTF-8)
  // We stop if we find a "Start Byte" or ASCII (value < 128)
  while (i > 0) {
    i--;
    const byte = buf[i];

    // If it's a Continuation Byte (0x80 - 0xBF), keep stepping back.
    if ((byte & 0xC0) === 0x80) continue;

    // We found the Start Byte (or ASCII). 
    // Now we perform the single check: "Does this char fit?"
    
    // Calculate expected size:
    // ASCII (0xxxxxxx) -> 1
    // 110xxxxx -> 2
    // 1110xxxx -> 3
    // 11110xxx -> 4
    let charLength = 1;
    if (byte >= 0xC0) charLength = 2;
    if (byte >= 0xE0) charLength = 3;
    if (byte >= 0xF0) charLength = 4;

    // If the sequence + start index fits within maxBytes, we are good!
    // Otherwise, we cut BEFORE this character (at i).
    if (i + charLength > maxBytes) {
      return buf.subarray(0, i).toString('utf8');
    }
    
    // If it fits, the cut was clean.
    break;
  }

  return buf.subarray(0, maxBytes).toString('utf8');
}

@X6TXY
Copy link
Copy Markdown
Contributor Author

X6TXY commented Feb 3, 2026

Got it, I’ll wait for other feedback and fix everything together.
Thanks for the review, @vicb!

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Feb 3, 2026

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@12378

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@12378

miniflare

npm i https://pkg.pr.new/miniflare@12378

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@12378

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@12378

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@12378

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@12378

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@12378

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@12378

wrangler

npm i https://pkg.pr.new/wrangler@12378

commit: 465de13

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 3 additional flags.

Open in Devin Review

@X6TXY X6TXY requested a review from vicb February 3, 2026 15:30
Copy link
Copy Markdown
Contributor

@vicb vicb left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks for your work on this 🎉

@github-project-automation github-project-automation Bot moved this from Untriaged to Approved in workers-sdk Feb 5, 2026
@vicb vicb added the skip-pr-description-validation Skip validation of the required PR description format label Feb 5, 2026
@vicb vicb merged commit 18c0784 into cloudflare:main Feb 5, 2026
41 of 43 checks passed
@github-project-automation github-project-automation Bot moved this from Approved to Done in workers-sdk Feb 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip-pr-description-validation Skip validation of the required PR description format

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Cloudflare Pages deployment fails with "Invalid commit message" for long multi-line UTF-8 commit messages

3 participants