Skip to content

.as('hidden', booleanValue) silently submits as false (regression in 2.60.0, follow-up to #15802) #15840

@hjaber

Description

@hjaber

Describe the bug

.as('hidden', booleanValue) (added in #15802, shipped 2.60.0) renders fine, but the receive path was missed. convert_formdata only knows the 'on' literal that checkboxes emit, so <input value="true"> lands as false server-side. The schema validates either way, so it's silent — handler runs with the wrong data.

true always corrupts to false. false coincidentally lands as false. Number .as('hidden', n) is unaffected (parseFloat handles it).

Reproduction

No app needed — the bug is one function call:

import { convert_formdata } from '@sveltejs/kit/src/runtime/form-utils.js';

const fd = new FormData();
fd.append('b:flag', 'true');  // exactly what `.as('hidden', true)` puts in the DOM
console.log(convert_formdata(fd));
// → { flag: false }   (expected: { flag: true })

End-to-end repro (bun create svelte, paste these two files, submit the form, watch the server log say false):

// src/routes/repro.remote.ts
import { form } from '$app/server';
import * as v from 'valibot';

export const submit = form(
  v.object({ flag: v.boolean() }),
  async (data) => { console.log('server:', data); return data; }
);
<!-- src/routes/+page.svelte -->
<script>
  import { submit } from './repro.remote';
  let flag = $state(true);
  let result = $state();
</script>

<form {...submit.enhance(async ({ submit }) => { result = await submit(); })}>
  <input {...submit.fields.flag.as('hidden', flag)} />
  <button type="submit">submit (flag={flag})</button>
</form>
<pre>{JSON.stringify(result)}</pre>

Root cause

packages/kit/src/runtime/form-utils.js:

// emit path (added by #15802) — picks 'b:' prefix for boolean hidden inputs
function get_type_prefix(field_type, is_array, input_value) {
  if (field_type === 'hidden' || field_type === 'submit') {
    if (typeof input_value === 'boolean') return 'b:';   // → <input name="b:flag" value="true">
  }
  // ...
}

// receive path in convert_formdata — still assumes the only writer of `b:` is <input type="checkbox">
} else if (key.startsWith('b:')) {
  values = values.map((v) => v === 'on');   // 'true' === 'on' → false
}

#15802 added a second writer for b: keys but didn't teach the receiver about it. The PR's added test (as-value/form.remote.ts) only checks that v.boolean() accepts the data — and it does, because false is a valid boolean — so the regression slipped through.

Suggested fix

values = values.map((v) => v === 'on' || v === 'true');

…plus a roundtrip test that asserts true stays true.

Workaround

Stay on .as('checkbox') + hidden attr for boolean fields — 'on' round-trips correctly. Schema needs v.optional(v.boolean(), false) because unchecked checkboxes are omitted from FormData.

System Info

  System:
    OS: Linux 6.17 Ubuntu 24.04.4 LTS
    CPU: (4) x64 AMD EPYC-Genoa Processor
  Binaries:
    Node: 24.14.0
    npm: 11.9.0
    bun: 1.3.14
  npmPackages:
    @sveltejs/kit: 2.60.1 => 2.60.1
    @sveltejs/vite-plugin-svelte: ^7.1.2 => 7.1.2
    svelte: ^5.55.7 => 5.55.7
    vite: 8.0.13 => 8.0.13

Severity

serious, can work around it

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions