Module: github.com/cryguy/worker v1.0.0
Date: 2026-02-22
5 bugs identified across URL parsing, fetch/abort, KV, and URLSearchParams. All are in the worker module's JS polyfills or Go-backed bindings. Ordered by severity.
Severity: High
File: kv.go — buildKVBinding, get handler (around line 93)
Also affects: interfaces.go — KVStore interface
await env.TEST_KV.put('key', '');
const val = await env.TEST_KV.get('key');
console.log(val); // null — expected ''The binding treats empty string as "not found":
val, err := store.Get(key)
// ...
if val == "" {
resolver.Resolve(v8.Null(iso))
return resolver.GetPromise().Value
}The KVStore.Get() interface returns (string, error). Go's zero-value string "" is used for both "key not found" and "key exists with empty value", so the binding can't distinguish them.
Change the interface to use a pointer:
// interfaces.go
type KVStore interface {
Get(key string) (*string, error) // nil = not found, non-nil = value (even if empty)
// ...
}Then in kv.go:
val, err := store.Get(key)
if err != nil { /* reject */ }
if val == nil {
resolver.Resolve(v8.Null(iso))
return resolver.GetPromise().Value
}
// Use *val from hereNote: This is a breaking interface change. All KVStore implementations will need to return nil for not-found and &value for found entries.
Severity: High
File: fetch.go lines 316–324
// AbortController — never aborts
const controller = new AbortController();
const promise = fetch('https://httpbin.org/delay/5', { signal: controller.signal });
setTimeout(() => controller.abort(), 100);
await promise; // Resolves normally after 5s instead of aborting at 100ms
// AbortSignal.timeout — never times out
await fetch('https://httpbin.org/delay/5', { signal: AbortSignal.timeout(100) });
// Resolves normally after 5sThe fetch Go callback blocks the V8 thread:
resultCh := make(chan fetchResult, 1)
go func() {
resp, err := client.Do(httpReq)
resultCh <- fetchResult{resp: resp, err: err}
}()
result := <-resultCh // ← Blocks the V8 threadWhile blocked on <-resultCh:
setTimeoutcallbacks cannot fire (event loop is blocked)AbortSignal.timeout()timer callbacks cannot fire- The JS abort listener that calls
__fetchAbortis never executed
The Go-level cancellation mechanism (fetchCtx, callFetchCancel) is correctly implemented — the issue is that the V8 event loop can't deliver the abort event to trigger it.
Integrate the fetch with the event loop instead of blocking:
- Start the goroutine as-is
- Return the unresolved promise immediately
- Have the goroutine notify the event loop (via channel/callback) when the HTTP response arrives
- The event loop processes the result and resolves/rejects the promise
This way, setTimeout and abort listeners can fire while the HTTP request is in-flight.
Severity: Medium
File: webapi.go — URLSearchParams constructor in webAPIsJS
const url = new URL('https://example.com/search?q=hello+world');
console.log(url.searchParams.get('q')); // 'hello+world' — expected 'hello world'class URLSearchParams {
constructor(init) {
// ...
for (const pair of s.split('&')) {
const [k, ...rest] = pair.split('=');
this._entries.push([decodeURIComponent(k), decodeURIComponent(rest.join('='))]);
}
}
}decodeURIComponent('hello+world') returns 'hello+world' literally. The WHATWG URL spec requires that URLSearchParams decode + as space (U+0020) in query strings.
Replace decodeURIComponent(x) with decodeURIComponent(x.replace(/\+/g, '%20')) for both key and value:
this._entries.push([
decodeURIComponent(k.replace(/\+/g, '%20')),
decodeURIComponent(rest.join('=').replace(/\+/g, '%20'))
]);Severity: Low
File: webapi.go — Request class in webAPIsJS + parseURL() function
const req = new Request('https://example.com');
console.log(req.url); // 'https://example.com' — expected 'https://example.com/'Two issues stack:
-
Requestconstructor just stores the raw string:this.url = String(input);
Per the Fetch spec,
new Request(url)should parse and serialize the URL, normalizing it. -
parseURL()returns emptyPathnamefor bare origins:// url.Parse("https://example.com") gives u.Path == "" Pathname: u.Path, // "" instead of "/"
-
Normalize URL in
Requestconstructor:this.url = new URL(String(input)).href;
-
Default empty path to
/inparseURL():pathname := u.Path if pathname == "" { pathname = "/" }
Severity: Low
File: webapi.go — urlParsed struct, parseURL() function, URL class in webAPIsJS
const url = new URL('https://user:pass@example.com:8443/path');
console.log(url.username); // undefined — expected 'user'
console.log(url.password); // undefined — expected 'pass'The urlParsed struct omits username and password:
type urlParsed struct {
Href string `json:"href"`
Protocol string `json:"protocol"`
Hostname string `json:"hostname"`
Port string `json:"port"`
Pathname string `json:"pathname"`
Search string `json:"search"`
Hash string `json:"hash"`
Origin string `json:"origin"`
Host string `json:"host"`
// Missing: Username, Password
}Go's url.Parse() correctly extracts these (u.User.Username(), u.User.Password()), but parseURL() doesn't include them, and the JS URL class never assigns this.username or this.password.
Add to struct:
type urlParsed struct {
// ... existing fields ...
Username string `json:"username"`
Password string `json:"password"`
}In parseURL():
var username, password string
if u.User != nil {
username = u.User.Username()
password, _ = u.User.Password()
}In the JS URL class constructor:
this.username = parsed.username || '';
this.password = parsed.password || '';| # | Bug | Severity | Breaking Change |
|---|---|---|---|
| 1 | KV.get() null for empty string | High | Yes (interface) |
| 2 | fetch blocks V8, abort never fires | High | No |
| 3 | URLSearchParams + not decoded |
Medium | No |
| 4 | Request URL not normalized | Low | No |
| 5 | URL missing username/password | Low | No |