Known issues
This page lists the defects that are open in the release these docs describe, grouped by module, with a workaround where one exists. Every issue is open: a design decision or a fix is pending.
The canonical list is
KNOWN_ISSUES.md at
the repository root. Each entry there has a stable ID (KI-NNN, never reused),
the evidence, and the next step. An entry stays until its fix is merged; the
fix removes the entry in the same change, and when the fix is visible to users,
the module’s CHANGELOG.md records it. If this page and that file disagree,
the file is right.
KI-035: a recovered handler panic is logged without the request ID
Section titled “KI-035: a recovered handler panic is logged without the request ID”When a handler panics, the router recovers and logs the panic through the
gas.Logger registered in the container. gas.RequestLogger[T] puts its
request_id-enriched logger on the request context and under T, never under
gas.Logger, so the panic line carries no request_id even with a request ID
middleware and gas.RequestLogger mounted.
Workaround: none in Gas. The stack trace is also written to stderr, so correlate by time if you need to.
KI-039: gas.WithServiceInstance with a nil interface panics at Start
Section titled “KI-039: gas.WithServiceInstance with a nil interface panics at Start”Registering a nil interface value, such as a gas.ConfigProvider variable that
holds nil, is accepted at registration and panics at Start with
reflect: call of reflect.Value.Interface on zero Value.
Workaround: check the value before you register it.
KI-045: routes inside Group and Route lose the router’s validator and form decoder
Section titled “KI-045: routes inside Group and Route lose the router’s validator and form decoder”A sub-router created by Group or Route does not copy the parent router’s
validator or form decoder, so its handlers get a fresh decoder and validator
on each request. Form binding there ignores form tags and rejects unknown
keys, and validation rules registered on the router’s validator are absent.
A field tagged form:"user_name" posted as user_name=bob&extra=1 binds on a
top-level route and fails with 400 invalid_form inside a group.
Workaround: register handlers that bind forms or rely on custom validation rules directly on the top-level router.
KI-050: RequestLogger[T] silently keeps a concrete scoped T
Section titled “KI-050: RequestLogger[T] silently keeps a concrete scoped T”gas.RequestLogger[T] puts the per-request logger into the request scope’s
slot for T only when the logger’s type is assignable to T. For a concrete
T, such as struct{ gas.Logger }, it keeps the original logger without an
error or a log line, so code resolving T gets no request_id. The logger on
the request context does carry it.
Workaround: declare T as an interface, for example
type RequestLogger interface{ gas.Logger }, as the examples do.
KI-054: RestartService revives routes the restarted Init did not re-register
Section titled “KI-054: RestartService revives routes the restarted Init did not re-register”When a service is closed with CloseService, its routes answer 503. When it
is restarted, its first route registration lifts the 503 for all of its old
routes, and every route its first Init registered is served again with the
original handler, even one the new Init did not register. Routes() lists
only what the new Init registered. A path re-registered with a different
handler is likely affected too; that case is untested.
Workaround: register the same routes on every Init of a service that can
be restarted. See Ownership and teardown.
KI-056: Routes() misreports global named middleware for routes registered before the Use
Section titled “KI-056: Routes() misreports global named middleware for routes registered before the Use”Named middleware added with a top-level Use runs on every route, wherever
the Use call sits. But Routes(), and the route map logged at startup, list
it only on top-level routes registered after the Use (and on grouped
routes), so a route registered before it is reported without it. Serving is
correct; only the report is wrong.
Workaround: call Use before registering routes when you rely on
Routes() or the route-map log.
KI-058: FieldError godoc names only the json tag
Section titled “KI-058: FieldError godoc names only the json tag”The FieldError.Field godoc says it holds the json tag name. The validator
uses the json tag, falls back to the form tag when there is none, and
falls back to the Go field name when neither is set or the json tag is -.
Only the documentation is wrong.
Workaround: rely on the full rule in Errors.
Config
Section titled “Config”KI-046: config.Load does not apply the Secrets Manager provider’s timeout
Section titled “KI-046: config.Load does not apply the Secrets Manager provider’s timeout”Config.Load loads providers with context.Background() through their
context-aware LoadContext, and only the Secrets Manager provider’s own
Load applies DefaultTimeout or WithTimeout. An endpoint that accepts and
never answers blocks config.Load with no deadline from the provider.
Workaround: call cfg.LoadWithContext(ctx) with a context that has a
deadline. See Config.
KI-047: the JSON provider accepts a file holding null
Section titled “KI-047: the JSON provider accepts a file holding null”A JSON config file whose content is null loads as no values instead of
failing with ErrJSONDecodeFailed, as a top-level array, string, number, or
boolean does.
Workaround: mark required fields with validate:"required", so binding
fails when the file supplied nothing.
KI-055: env and .env separators with uppercase letters never match
Section titled “KI-055: env and .env separators with uppercase letters never match”The environment and .env providers lowercase every variable name before
splitting it on the separator, but keep the separator as given. A separator
with an uppercase letter, such as WithEnvSeparator("_X_"), never matches, so
PROBEA_X_B loads as the flat key probea_x_b instead of nesting.
Workaround: use a separator without uppercase letters, such as the default
__.
Logging
Section titled “Logging”KI-041: log.WithEventInitialCapacity with a negative value panics
Section titled “KI-041: log.WithEventInitialCapacity with a negative value panics”The option stores the value as given, and the first log call then panics with
makeslice: cap out of range.
Workaround: pass zero or a positive capacity.
KI-042: log.Handler cannot be built outside the package
Section titled “KI-042: log.Handler cannot be built outside the package”log.Handler is exported, but its fields are unexported and no constructor
returns one, so it cannot be composed into a custom handler chain as its
documentation suggests.
Workaround: use log.NewShippingHandler, which returns a slog.Handler.
See Logging.
Database
Section titled “Database”KI-038: Close leaks the pgx pool when closing the sql.DB fails
Section titled “KI-038: Close leaks the pgx pool when closing the sql.DB fails”database.Service.Close returns as soon as closing the *sql.DB fails, before
it closes the pgx pool, so in pgx mode the pool stays open.
Workaround: none. It matters only when closing the *sql.DB fails, at
shutdown.
KI-043: database health errors use the wrong prefix
Section titled “KI-043: database health errors use the wrong prefix”CheckHealth and CheckReady return errors starting with database:
(database: closed, database: not initialized), while every other error of
the package starts with the service name, gas/database.
Workaround: if you match on error text, match these two as they are.
Migrations
Section titled “Migrations”KI-048: migrate on MySQL requires parseTime=true
Section titled “KI-048: migrate on MySQL requires parseTime=true”The tracking table’s applied_at column is scanned into a time.Time.
Without parseTime=true in the MySQL DSN, the first RunPending succeeds and
every later read of the tracking table fails with
sql: Scan error on column index 5, name "applied_at".
Workaround: add parseTime=true to the MySQL DSN. See
Migrations.
KI-057: migrate keeps a duplicate of a service’s migrations on every restart
Section titled “KI-057: migrate keeps a duplicate of a service’s migrations on every restart”A service registers its migrations in Init, and RestartService runs Init
again on the same instance, so every restart appends another copy of the
service’s migrations. Identical copies are collapsed when they are read, so
no migration runs twice, but the list grows by one copy per restart for the
life of the process.
Workaround: none needed for correctness; only memory grows, and only for a service that is restarted.
KI-040: apikey Generate panics on a short KeyLength from WithConfig
Section titled “KI-040: apikey Generate panics on a short KeyLength from WithConfig”Generate slices the generated key at the prefix length plus 8 characters.
With KeyLength below 6 bytes the encoded key is shorter than that, and the
slice panics. A config bound from the ConfigProvider is protected by its
validate:"min=16" tag, but a config given with apikey.WithConfig is used as
given.
Workaround: keep KeyLength at 16 or more in a config passed with
WithConfig, as the bound config requires.
KI-044: the apikey MySQL migration fails on MySQL 8.4
Section titled “KI-044: the apikey MySQL migration fails on MySQL 8.4”The migration create api keys (version 20250323002) declares
scopes TEXT NOT NULL DEFAULT '', and MySQL rejects a literal default on a
TEXT column with Error 1101 (42000). The session and token stores migrate
cleanly.
Separately, each auth MySQL migration file holds several statements and
gas/migrate runs a file in one call, so on MySQL the DSN needs
multiStatements=true for any of them to apply.
Workaround: for API keys, use PostgreSQL or SQLite. See Auth.
KI-052: cache/valkey Init’s context does not bound the dial
Section titled “KI-052: cache/valkey Init’s context does not bound the dial”The Valkey client is created without a context, so against a server that
accepts the connection and never answers, Init runs until
cache.dialtimeout ends the attempt, whatever the deadline of its context.
Measured: 30s with a 300ms context and a 30s dialtimeout.
Workaround: keep cache.dialtimeout short; it is the bound that applies.
See Cache.
Storage
Section titled “Storage”KI-051: storage/s3 option comments say a nil client is ignored
Section titled “KI-051: storage/s3 option comments say a nil client is ignored”WithClient, WithPresignerS3Client, and WithPresignClient store their
value, nil included, so a nil passed after a real client discards it and the
constructor builds a client from the configuration. Their doc comments say a
nil client is ignored.
Workaround: do not pass nil to these options. See Storage.
KI-034: gas.Email.Headers is never sent by email/ses
Section titled “KI-034: gas.Email.Headers is never sent by email/ses”ses.Service.Send builds the SES request from the recipients, subject, and
bodies and never reads Headers, so custom headers are dropped without an
error.
Workaround: none. Do not rely on custom headers with the SES backend. See Email.
KI-036: ses.Service.Send panics before Init
Section titled “KI-036: ses.Service.Send panics before Init”The SES client is created in Init. On a service that was constructed but
never initialized, Send dereferences a nil client and panics instead of
returning an error.
Workaround: let the container build the service, which runs Init before
handing it out. This bites only code that calls the constructor by hand.
KI-036: sqs.Service methods panic before Init
Section titled “KI-036: sqs.Service methods panic before Init”The same defect as in email: Enqueue, Dequeue, Ack, and Nack on an
sqs.Service that was never initialized panic on a nil client.
Workaround: as above. See Queue.
Templates
Section titled “Templates”KI-033: template/dir.Store cannot be registered as gas.TemplateProvider
Section titled “KI-033: template/dir.Store cannot be registered as gas.TemplateProvider”dir.Store declares Close() error and has no Name or Init, so the
container rejects it at Start with
*dir.Store declares Close but does not implement gas.Service.
Workaround: build the store yourself, wrap it in a composite.Store, and
close it yourself. Templates shows
the code.
KI-049: the template/db MySQL migration needs multiStatements=true
Section titled “KI-049: the template/db MySQL migration needs multiStatements=true”The store’s MySQL migration file holds a CREATE TABLE and a CREATE INDEX,
and gas/migrate runs a file as one statement batch. Without
multiStatements=true in the DSN, the migration fails with Error 1064 at the
CREATE INDEX. Same mechanism as KI-044.
Workaround: add multiStatements=true (and parseTime=true, for KI-048)
to the MySQL DSN. See Templates.
KI-032: RenderFragment renders nothing for a page that only defines content
Section titled “KI-032: RenderFragment renders nothing for a page that only defines content”RenderFragment executes only the page’s text outside its {{define}}
blocks. A page written to the usual convention, with all its markup in
{{define "content"}}, renders an empty fragment, so one template cannot
serve both Render and RenderFragment that way.
Workaround: put the markup in a partial and call it both from the
content block and from the page’s top level.
UI shows the templates.
KI-053: ui keeps serving errors after a failed recompile
Section titled “KI-053: ui keeps serving errors after a failed recompile”After the templates have compiled once, a failed recompile, such as one that
RegisterFuncs triggers while some page has a parse error, makes every later
render retry the build and return the error. The previously compiled
templates stay in memory but are never served, in production as well as in
development.
Workaround: fix the template; the next successful compile restores
rendering. Compile every template in a test, for example with ui.NewEngine
and Build, so a parse error fails before deployment.
Tests only
Section titled “Tests only”KI-010: suspected flake in the S3 multipart content-length test
Section titled “KI-010: suspected flake in the S3 multipart content-length test”One case of TestProvider_PutObjectContentLength in storage/s3 failed once
in about 31 runs when the in-process fake S3 hit a transport error that masked
the expected ErrContentLengthMismatch. The runtime behavior is correct; only
the test assertion is fragile.