Skip to content

fix: coerce empty maps structurally instead of corrupting string payloads#25

Merged
abnegate merged 1 commit into
mainfrom
fix-empty-map-coercion-corrupts-strings
Jun 11, 2026
Merged

fix: coerce empty maps structurally instead of corrupting string payloads#25
abnegate merged 1 commit into
mainfrom
fix-empty-map-coercion-corrupts-strings

Conversation

@abnegate

Copy link
Copy Markdown
Member

The bug

K8sResource::toJsonPayload() coerced empty PHP arrays ([]) into empty JSON objects ({}) for Kubernetes map fields using a blind global string replace over the entire JSON-encoded payload:

$attributes = str_replace(': []', ': {}', $attributes);

Because this operates on the raw JSON string, it also rewrites the : [] byte sequence wherever it appears inside string values — not just at structural map positions. Container command arrays that embed inline scripts are corrupted.

Repro

$container = K8s::container()
    ->setName('backup')
    ->setImage('php', '8.4')
    ->setCommand(['php', '-r', 'foreach (glob("/x/*") ?: [] as $file) { echo $file; }']);

$job = $cluster->job()->setName('backup')->setTemplate(
    $cluster->pod()->setName('backup')->setContainers([$container])
);

$cmd = json_decode($job->toJsonPayload(), true)['spec']['template']['spec']['containers'][0]['command'][2];
// Before: foreach (glob("/x/*") ?: {} as $file) { echo $file; }   <-- PHP parse error
// After:  foreach (glob("/x/*") ?: [] as $file) { echo $file; }

The PHP expression glob(...) ?: [] json-encodes as ?: [] inside the command string and gets rewritten to ?: {}, which is a syntax error (unexpected token "{").

The fix

Replace the blind string replace with a structure-aware coercion. Instead of rewriting the encoded string, walk the resource attribute tree before encoding and convert empty-array nodes to empty objects. String scalars are leaves in the tree and are left byte-identical, so embedded scripts are never touched:

$coerced = $this->coerceEmptyArraysToObjects($this->toArray($kind));
return json_encode($coerced, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);

The recursive walk preserves the exact prior semantics: every empty array in the structure becomes {}, with the same three genuine list fields (allowedTopologies, mountOptions, accessModes) exempted by key name at any depth — previously handled by follow-up str_replace calls that re-inverted them. The public contract of toJsonPayload() is unchanged.

Impact on Appwrite Edge

Appwrite Edge provisions dedicated-database backup Jobs whose containers run inline php -r maintenance scripts containing glob(...) ?: []. With the old coercion, every such Job was serialized with corrupted command strings and crash-looped on startup with Parse error: syntax error, unexpected token "{", breaking dedicated-database backups in production. This fix makes those Jobs serialize correctly while preserving all existing empty-map behavior.

Tests

Added tests/JsonPayloadTest.php:

  • string values containing ?: [] (and : [] generally) serialize byte-identical — fails on the old code, passes now
  • empty data/labels maps still coerce to {}
  • the three exempted list fields stay []
  • nested empty maps deep in spec coerce to {}
  • non-empty list arrays stay lists

The two string-corruption tests fail against the unfixed method and pass with the fix; the full suite shows no new failures (remaining errors are pre-existing and require a live cluster / ext-yaml).

🤖 Generated with Claude Code

…oads

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 11, 2026 07:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@greptile-apps

greptile-apps Bot commented Jun 11, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a production bug where K8sResource::toJsonPayload() corrupted container command strings containing the literal byte sequence ": []" (e.g. glob(...) ?: [] in inline PHP scripts) by performing a blind global str_replace over the entire JSON-encoded string. The fix replaces that approach with a structure-aware recursive walk over the PHP attribute tree before encoding, so only empty arrays at structural positions are coerced to stdClass objects — string scalars are leaves and are never touched.

  • Core fix (src/Kinds/K8sResource.php): toJsonPayload() now calls coerceEmptyArraysToObjects(), a protected recursive method that walks the PHP tree and casts empty arrays to (object) [], exempting the same three list-field keys (allowedTopologies, mountOptions, accessModes) that the old str_replace re-inversion handled.
  • Tests (tests/JsonPayloadTest.php): Five new test cases cover the string-corruption regression, the generic ": []" literal preservation, non-empty list arrays staying as lists, empty map coercion, field exemptions, and deep nested coercion.

Confidence Score: 4/5

Safe to merge — the structural coercion correctly replaces the string-manipulation approach and all three previous exemptions are preserved.

The implementation is sound: string scalars inside the attribute tree are never touched, empty map fields still serialize to {}, and the exempted list fields stay as []. The one minor note is that $emptyArrayLists is rebuilt on every recursive frame rather than being declared once. No incorrect output was found for any tested or reasoned-about input shape.

No files require special attention; both changed files look correct.

Important Files Changed

Filename Overview
src/Kinds/K8sResource.php Replaces blind string-replace coercion with a recursive PHP tree walk; logic is correct, return type is unchanged, and all pre-existing exemptions are preserved.
tests/JsonPayloadTest.php New test file with five targeted cases covering the string-corruption regression, empty map coercion, exempted list fields, and deep nesting; all tests are well-scoped and require no live cluster.

Reviews (1): Last reviewed commit: "fix: coerce empty maps structurally inst..." | Re-trigger Greptile

Comment thread src/Kinds/K8sResource.php
*/
protected function coerceEmptyArraysToObjects(array $attributes): array
{
$emptyArrayLists = ['allowedTopologies', 'mountOptions', 'accessModes'];

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 The $emptyArrayLists array literal is allocated fresh on every recursive invocation. For a deeply nested resource structure this creates unnecessary allocations on each stack frame. Declaring it as a static local avoids the per-call cost and makes the intent clearer.

Suggested change
$emptyArrayLists = ['allowedTopologies', 'mountOptions', 'accessModes'];
static $emptyArrayLists = ['allowedTopologies', 'mountOptions', 'accessModes'];

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@abnegate abnegate merged commit f571fd2 into main Jun 11, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants