@@ -415,6 +415,93 @@ async fn test_url_search_params_as_fetch_body() {
415415 . await ;
416416}
417417
418+ // ============================================================================
419+ // URLSearchParams + fetch integration
420+ // ============================================================================
421+
422+ #[ tokio:: test]
423+ async fn test_fetch_url_search_params_body_serialization ( ) {
424+ run_local ( || async {
425+ let result = eval_js (
426+ r#"addEventListener('fetch', async (event) => {
427+ // Monkey-patch __nativeFetchStreaming to capture what fetch() sends
428+ const captured = {};
429+ const origFetch = __nativeFetchStreaming;
430+ globalThis.__nativeFetchStreaming = function(opts, resolve, reject) {
431+ captured.body = opts.body;
432+ captured.headers = opts.headers;
433+ // Call resolve with a fake response
434+ resolve({ status: 200, statusText: 'OK', headers: {}, streamId: null });
435+ };
436+
437+ const params = new URLSearchParams({
438+ grant_type: 'authorization_code',
439+ client_id: 'my-client',
440+ code: 'abc123'
441+ });
442+
443+ await fetch('https://example.com/token', {
444+ method: 'POST',
445+ body: params
446+ });
447+
448+ // Restore
449+ globalThis.__nativeFetchStreaming = origFetch;
450+
451+ // Verify the body was serialized to Uint8Array
452+ const bodyStr = new TextDecoder().decode(captured.body);
453+ const ct = captured.headers['Content-Type'] || '';
454+
455+ event.respondWith(new Response(JSON.stringify({ body: bodyStr, ct })));
456+ });"# ,
457+ )
458+ . await ;
459+
460+ let parsed: serde_json:: Value = serde_json:: from_str ( & result) . unwrap ( ) ;
461+ assert_eq ! (
462+ parsed[ "body" ] ,
463+ "grant_type=authorization_code&client_id=my-client&code=abc123"
464+ ) ;
465+ assert ! (
466+ parsed[ "ct" ]
467+ . as_str( )
468+ . unwrap( )
469+ . starts_with( "application/x-www-form-urlencoded" )
470+ ) ;
471+ } )
472+ . await ;
473+ }
474+
475+ #[ tokio:: test]
476+ async fn test_fetch_url_search_params_does_not_override_explicit_content_type ( ) {
477+ run_local ( || async {
478+ let result = eval_js (
479+ r#"addEventListener('fetch', async (event) => {
480+ const captured = {};
481+ const origFetch = __nativeFetchStreaming;
482+ globalThis.__nativeFetchStreaming = function(opts, resolve, reject) {
483+ captured.headers = opts.headers;
484+ resolve({ status: 200, statusText: 'OK', headers: {}, streamId: null });
485+ };
486+
487+ await fetch('https://example.com/token', {
488+ method: 'POST',
489+ headers: { 'Content-Type': 'text/plain' },
490+ body: new URLSearchParams({ key: 'value' })
491+ });
492+
493+ globalThis.__nativeFetchStreaming = origFetch;
494+ event.respondWith(new Response(captured.headers['Content-Type']));
495+ });"# ,
496+ )
497+ . await ;
498+
499+ // Explicit Content-Type should be preserved
500+ assert_eq ! ( result, "text/plain" ) ;
501+ } )
502+ . await ;
503+ }
504+
418505// ============================================================================
419506// btoa / atob (binary strings, NOT UTF-8)
420507// ============================================================================
0 commit comments