Diagnostics
Who this is for: anyone looking at a message servo just printed and wanting to know what to change.
Servo’s position on failure is that a missing dependency, an ambiguous binding, or a cycle is a build error with a source position — never a runtime panic and never a guess. This page lists every message it can produce, grouped by the stage that produces it.
All of them go to stderr, and all of them exit 1 — with one deliberate exception, servo-vet’s
refusal of -tags, which exits 2 because it is the tool declining to run
rather than a finding about your code.
Reproducing them yourself
examples/diagnostics is eight
small, permanently broken fixtures — one per failure mode — each runnable on its own:
go run ./cmd/servo generate --dir examples/diagnostics/missing
go run ./cmd/servo generate --dir examples/diagnostics/ambiguous
go run ./cmd/servo generate --dir examples/diagnostics/cycle
go run ./cmd/servo generate --dir examples/diagnostics/widening
go run ./cmd/servo generate --dir examples/diagnostics/crossscope
go run ./cmd/servo generate --dir examples/diagnostics/extractor
go run ./cmd/servo generate --dir examples/diagnostics/undeclared
go run ./cmd/servo generate --dir examples/diagnostics/noscopekey
Every example below is that command’s real output, with two consistent abbreviations for width:
absolute file paths are shortened to their module-relative form, and fully qualified type names to
their last two segments — missing.Store, not example.com/servodiagnostics/missing.Store. The
tool always prints the full path.
Anatomy of a resolution diagnostic
example.com/servodiagnostics/missing: servo: 1 diagnostic(s):
missing/server.go:11:6: servo: no provider for missing.Store
needed by *missing.Server missing/server.go:11:6
root missing/spec.go:9:3
Four parts, worth reading in this order:
- The package path prefix — which injector failed. Only
generateadds it, because onlygenerateprocesses several injectors in one pass. Every failed injector is reported, not just the first. N diagnostic(s)— resolution collects every failure it can before giving up, so one run usually tells you everything.file:line:col:— the position, in the form editors and CI annotations already parse. It is the declaration site of the component that couldn’t have its dependency satisfied.- The chain — one
needed byline per consumer, immediate consumer first, then exactly onerootline pointing at theservo.Root[…]()call this traversal descended from. That last line is the answer to “why is this even being built”.
A node that is both a root and, from its dependency’s point of view, a consumer gets both lines. That’s not a duplicate.
Resolution
No provider
missing/server.go:11:6: servo: no provider for missing.Store
needed by *missing.Server missing/server.go:11:6
root missing/spec.go:9:3
Nothing in the graph can produce that type. With no candidate list following, it means servo found zero possibilities — not several.
In order of likelihood:
- The constructor exists but wasn’t accepted. Run
servo list --rejectedand look for it. Every rejection reason is explained in Resolution rules. Being unexported, generic, variadic, a method, or returning an unsupported shape all land here. - The constructor is in a
_test.gofile. Test files are not scanned at all. - The type is slightly off.
Tand*Tare different nodes; a constructor returningpostgres.DBwill not satisfy a parameter of*postgres.DB. - It’s an interface with no implementation in the main module. The structural search covers your own module only.
- An explicit
Bindnames a type nothing produces. Then the reported type is the bound type, and no suggestions are printed — servo won’t second-guess an explicit instruction.
Ambiguous interface
ambiguous/store.go:23:6: servo: no provider for ambiguous.Store
needed by *ambiguous.Server ambiguous/store.go:23:6
root ambiguous/spec.go:9:3
2 types implement ambiguous.Store — add one of:
servo.Bind[ambiguous.Store, *ambiguous.Postgres]() ambiguous/store.go:11:6
servo.Bind[ambiguous.Store, *ambiguous.Redis]() ambiguous/store.go:17:6
Several types implement the interface, so auto-binding can’t pick one. The fix is printed
verbatim: copy one of those lines into your servo.Build(...) call.
Candidates are sorted by source position, so the list is stable across runs. If a listed candidate surprises you — a fake, a second implementation you forgot about — that’s information: it is a real type in your module that really does implement the interface.
Ambiguous concrete type
servo: no provider for *sqsaccounts.Client
2 functions produce *sqsaccounts.Client — remove or rename all but one:
processor.NewClientA processor/naive.go:5:6
processor.NewClientB processor/naive.go:6:6
Two functions return the same concrete type, with no interface involved. No Bind can fix
this — there’s nothing to disambiguate between, because identity in the graph is purely by type,
and two functions returning one type are not two instances but one contested node.
The fix is to make them distinct types:
type OrdersAccount struct{ *Client }
type AuditAccount struct{ *Client }
Embedding keeps every method available with no delegation boilerplate. The README’s Multiple instances of the same type walks through a complete worked example, and Limitations explains why there are no tags to solve it with.
A special case you can’t fix by deleting a function: ask for a stdlib or third-party concrete type and its own constructors become the candidates.
servo: no provider for *database/sql.DB
2 functions produce *database/sql.DB — remove or rename all but one:
sql.OpenDB database/sql/sql.go:837:6
sql.Open database/sql/sql.go:868:6
Write your own wrapper type and depend on that (servo new adapter postgres scaffolds one).
Dependency cycle
cycle/graph.go:8:6: servo: dependency cycle detected
*cycle.A cycle/graph.go:8:6
*cycle.B cycle/graph.go:12:6
*cycle.A cycle/graph.go:8:6 (cycle closes here, back to the first line)
The full loop is printed, closing on the type it started from. Constructor injection cannot express a
cycle: A needs a finished B, B needs a finished A, and there’s no ordering that satisfies
both. There is no lazy-injection escape hatch by design.
Break it by extracting what they share into a third type both depend on, or by having one side accept a callback the other registers after construction.
Unused supplied value
cmd/api/spec.go:13:3: servo: servo.Value[example.com/app/conf.Flags]() is declared, but nothing in
the graph depends on example.com/app/conf.Flags
A declared value becomes a field on the generated Values struct, so this
one would be supplied by every caller and read by nobody.
Two ways out:
- take it as a constructor parameter somewhere the roots reach:
func New(v example.com/app/conf.Flags) *Thing
- delete the servo.Value declaration
A servo.Value declares a parameter every caller of NewWith has to fill in, so
one nothing reads is a cost with no return. Reported against the marker’s own call site rather than
against the type, since the declaration is what has to change.
Checked against the production graph only. If a servo.Override swaps out the
only consumer of a supplied value, the test variant leaves the value off TestValues rather than
reporting it: a mock that takes fewer things than the real implementation is the ordinary case, not a
mistake in the spec file.
Spec file
Full explanations on Spec file and markers. In short:
| Message | Fix |
|---|---|
servo: no servo.Build(...) call found |
Run servo init |
multiple servo.Build(...) calls found in the same package |
Delete one; a package owns one generated file |
spec file is missing a `//go:build servoinject` constraint |
Add the tag |
servo.Build argument is not a marker call |
Only marker calls belong in Build |
servo.Build argument must be a Root/Bind/Override/Scoped call with explicit type arguments |
Write markers inline, not via a variable or a helper — or use servo.Include for a shared set |
unrecognized servo marker "X" inside Build(...) |
Not a marker function |
servo.Root expects exactly one type argument |
Root[T]() |
servo.Bind/Override expects exactly two type arguments |
Bind[I, C]() |
second type argument must be a concrete type, not an interface |
Name the implementation, not another interface |
servo.Bind[…] declared twice |
One bind per interface — unless the first came from an Include, in which case the local one wins silently |
servo.Value expects exactly one type argument |
Value[T]() |
servo.Value[…]() declared twice |
One Value per type |
servo.Include takes exactly one argument, the name of a func() []servo.Marker |
Include(pkg.Shared) |
servo.Include's argument must name a declared func() []servo.Marker |
Pass the function’s name, not a literal, a method value, or a call |
servo.Include names …, whose declaration is not in this build |
The included function is excluded by a constraint this run doesn’t satisfy |
servo.Include names …, which is declared in a file without a `//go:build servoinject` constraint |
Tag the shared marker set’s file, exactly like a spec file |
X must be exactly `return []servo.Marker{ ... }` |
The included body is read as syntax, so it can’t be a variable, a conditional, or an append |
servo.Include cycle — X includes itself, through: |
Two shared sets include each other; the path that closed the loop is printed |
servo.ConfigFile takes exactly one argument, a string literal path |
ConfigFile("config.yaml") |
servo.ConfigFile's argument must be a string literal |
The path is read as syntax; a variable or a call can’t be |
servo.ConfigFile(…): the extension must be .json, .yaml, .yml, or .toml |
It picks the decoder the generated code carries |
servo.ConfigFile(...) declared twice |
One file per injector; a local declaration may still replace an included one |
Configuration
The //servo:config directive adds two families of diagnostics. Scanning ones —
malformed directive or tag, unsupported field type, a bad default=, two directives in one
package — are module-wide errors reported at the declaration, whether or not any graph uses the
type: the author wrote the directive, and servo never silently half-honors one. Resolution ones —
a hand-written constructor for a config type, two used configs claiming one environment variable
or file key, a scoped constructor depending on a config, a ConfigFile nothing reads — are
per-graph. The full table, with causes and fixes, is on
Generated configuration.
Loading
Build errors outside the injector
servo: module has build errors:
<the go/packages errors>
Some package other than an injector’s own doesn’t type-check. Servo can’t resolve a graph it can’t type-check, so fix the compile error first.
Errors inside an injector’s own package are deliberately tolerated. On a fresh checkout, before
any generation has run, main.go legitimately references a New that doesn’t exist yet — that’s a
reason to generate, not a reason to refuse.
Missing runtime package
load: servo runtime package github.com/okian/servo/v3/servo is not imported by any file this
build configuration (the default one, with no build tags) can see — either no spec file exists
yet (run `servo init`), or every spec file is gated on a build constraint this configuration
does not satisfy, which makes it invisible to this run
Nothing this run can see imports the servo runtime, which for a module with an injector is only possible if the spec file was excluded. The two causes are named in the message because they need different fixes, and the parenthesis tells you which configuration you were in:
- No spec file yet. Run
servo init. - Every spec file is gated out. With variants, a spec constrained
//go:build servoinject && prodis invisible to a plainservo check, and one constrainedservoinject && !prodis invisible toservo check --tags=prod. The configuration is named verbatim —-tags=prod, orthe default one, with no build tags— so the fix is to add or drop the flags that spec is written for.
Also reachable by pointing --dir at a directory with no spec under it at all.
Multiple injectors in scope
servo: multiple injectors found in this scope — pass --dir to pick one:
cmd/basic/spec.go:16:2
cmd/migrator/spec.go:11:2
From graph, explain, why or list — commands that answer a question about one graph. Point
--dir at one injector’s own directory. generate, check and doctor process all of them and
never print this.
Scopes
These four are what a hand-written registry beside servo cannot give you. All are generate
failures, and all four carry the same needed-by chain a resolution diagnostic does. The
extractor-cycle chain leads to the scoped dependency rather than to the extractor, whose own
position is printed above it; the undeclared-scope chain comes from the traversal path that reached
the type, because that check fires while candidates are still being selected. See Scoped
instances for the feature they belong to.
They are the four named scope diagnostics. Thirty-seven narrower ones — a stray scope key, a node two scopes both claim, a scoped type declared as a root, a bound accessor — are tabulated under Other scope errors below.
Widening
widening/rooms.go:37:6: servo: *widening.Room is scoped, but *widening.Server is a
singleton that depends on it
needed by *widening.Server widening/rooms.go:37:6
root widening/spec.go:9:3
A singleton is constructed once and held for the life of the process, so it
would capture whichever *widening.Room happened to be built first and hand that same
one to every caller afterwards, whatever key they present. Nothing about the
running program would say so.
Two ways out:
- depend on the accessor instead: change widening.NewServer's parameter from *widening.Room to widening.Rooms,
and call Acquire(ctx) per request
- make *widening.Server scoped too, by giving it a dependency on widening.RoomKey
The one this feature exists for. A singleton holding a scoped instance pins one key’s instance for the life of the process — the first room anyone joins becomes everyone’s room — and it is invisible until production.
The usual fix is the first one: take the accessor interface and Acquire(ctx) inside the method
that needs it, so the reference lasts for one call rather than for the process.
Cross-scope
crossscope/nested.go:50:6: servo: *crossscope.Room and *crossscope.Tenant are in
different scopes
needed by *crossscope.Room crossscope/nested.go:50:6
root crossscope/spec.go:10:3
*crossscope.Room is keyed by crossscope.RoomKey
*crossscope.Tenant is keyed by crossscope.TenantKey
Nested scopes are deliberately not supported in this release: one instance
per key pair means two reference counts and two linger windows with no single
owner, and no obvious answer for what happens when the outer one evicts while
the inner one is still held. This is a rejection, not an oversight.
Depend on *crossscope.Tenant's accessor interface instead and Acquire it inside the method
that needs it, so the inner instance is held only for that call.
Nested scopes are rejected on purpose, not as a side effect of how the reachability pass happens to work. Holding the inner scope’s accessor is fine and is the intended answer — that edge crosses no scope boundary, because an accessor is not an instance.
Extractor cycle
extractor/session.go:36:17: servo: *extractor.Session's ScopeKey extractor depends on
*extractor.Decoder, which is itself scoped
ScopeKey extractor/session.go:36:17
needed by *extractor.Decoder extractor/session.go:25:6
needed by *extractor.Session extractor/session.go:32:6
root extractor/spec.go:10:3
The extractor is what decides which instance a caller gets, so it runs before
any instance exists. Everything it takes must already be constructed — that is,
a singleton.
A ScopeKey method may take dependencies after its ctx, and they resolve as ordinary graph
edges. They just cannot be scoped: choosing the instance is the thing the extractor is being called
to do.
Undeclared scope
undeclared/tenant.go:22:16: servo: *undeclared.Tenant declares a ScopeKey method but no
servo.Scoped declares it
ScopeKey undeclared/tenant.go:22:16
needed by *undeclared.Tenant undeclared/tenant.go:20:6
needed by *undeclared.Server undeclared/tenant.go:32:6
root undeclared/spec.go:9:3
A ScopeKey method is what makes a type keyed rather than a singleton, and servo
will not infer the rest of the declaration from it: the accessor interface has
to be one you name, because servo cannot emit a type into your package.
In package undeclared:
type Tenants interface {
Acquire(ctx context.Context) (*Tenant, func(), error)
}
In servo.Build:
servo.Scoped[*undeclared.Tenant, undeclared.Tenants](),
Or delete the ScopeKey method, if this type is meant to be an ordinary singleton.
The mirror image also fires — a servo.Scoped[T, I] whose T has no ScopeKey method names the
missing method and prints its required shape.
Other scope errors
Resolution stage — these carry the servo: prefix:
| Message | Cause |
|---|---|
servo: X.ScopeKey must not name its receiver |
Generated code calls it on a typed nil. Write func (*T) ScopeKey(...) |
servo: ScopeKey's first parameter must be context.Context |
The key comes from the request context |
servo: ScopeKey must return exactly (K, error) |
Without the error, a missing key becomes the zero K |
servo: ScopeKey must not be variadic |
Every parameter after ctx is a dependency, and a variadic one is a slice |
servo: ScopeKey's key type is X, which is not a defined type |
Scope identity is type identity; string cannot be one |
servo: ScopeKey's key type X is an interface |
Two callers’ dynamic types would never compare equal |
servo: ScopeKey's key type X is not comparable |
It keys the instance map |
servo: ScopeKey must be declared on the pointer receiver |
The node in the graph is *T, and a value receiver would dereference the typed nil |
servo: X is a scope key and is not resolvable outside its scope |
A singleton asked for the key type directly |
servo: X depends on K, which is a scope key |
A singleton took a scope’s key — usually a node reached only through a different scope’s sub-graph |
servo: X is keyed by K1 but depends on K2, another scope's key |
The same, for a node that is in a scope, just not that one |
servo: X belongs to two scopes at once |
Two scopes both claim one node. A nested scope by another route |
servo: servo.Root[X] declares a scoped type as a root |
A root is held by the App for the life of the process — widening, with the App as the consumer |
servo: servo.Root[I] declares a scope accessor as a root |
An accessor is generated code, not a node a root can pull in |
servo: I is a scope accessor interface and cannot be bound or overridden |
servo emits the value satisfying I; there is no selection for a Bind to change |
servo: F produces I, which is a scope accessor interface |
The same mistake made with a constructor: an accepted candidate resolution would never select |
servo: T's ScopeKey extractor takes I, its own scope's accessor |
Acquire calls the extractor, so acquiring from inside it recurses without bound. Another scope’s accessor is fine |
servo: scope accessor interface I cannot be satisfied |
I declares a method the generated accessor does not have, or one whose signature does not match |
servo: conflicting servo.Linger for scope K |
Two declarations share a key type — and therefore one registry — but disagree about its window |
servo: conflicting servo.Max for scope K |
The same, for the instance cap |
Spec-parsing stage — read as syntax, before resolution, and reported without the prefix:
| Message | Cause |
|---|---|
servo.Scoped expects exactly two type arguments |
Wrong arity |
servo.Scoped's first type argument must be the concrete scoped type, not an interface (X) |
An interface where the scoped type belongs |
servo.Scoped's first type argument must be a pointer, not X |
Acquire reports failure by returning a nil instance beside the error, and a value type has no nil to return |
servo.Scoped's second type argument must be an interface |
A concrete type where the accessor interface belongs |
servo.Scoped's accessor interface I declares no methods |
any is satisfied by everything |
servo.Scoped[T, ...] declared twice |
One scoped type, one declaration |
servo.Scoped[..., I] declared twice |
One accessor interface cannot stand for two scoped types |
servo.Scoped's arguments must be servo.Linger(...) or servo.Max(...) calls |
Something else in the option list |
servo.X is not a scope option |
A servo marker that is neither Linger nor Max |
servo.Linger is a scope option, not a Build marker |
An option at the top level of Build |
servo.Linger/Max declared twice in the same servo.Scoped |
One value each per declaration |
servo.Linger/Max expects exactly one argument |
Wrong arity |
servo.Linger's argument must be a constant expression |
The spec file is read as syntax, never executed |
servo.Linger's argument must be a constant time.Duration |
The folded constant is not an integer |
servo.Max's argument must be a constant integer |
The same, for Max |
servo.Linger(...) must not be negative |
Use servo.Linger(0) for die-with-the-last-holder |
servo.Max(N) must be positive |
A scope that can hold no instances can never hand one out |
HTTP routes
The //servo: comment prefix is reserved in full, so nothing under it can silently no-op: a typo’d
method is an error, not an unserved route. The scan runs module-wide on every load, whether or not
any spec declares servo.HTTP().
The directive itself:
| Message | Cause |
|---|---|
unknown //servo: directive "pots" |
The method must be one of get/post/put/patch/delete/head/options |
//servo:post needs a route pattern |
No pattern after the method |
route pattern "order" must start with "/" |
Patterns are rooted |
//servo:post takes exactly one pattern |
Trailing text after the pattern |
route GET /a/{x}suffix cannot be registered on net/http.ServeMux: … |
net/http’s own words, for a malformed pattern — the same registration that would panic at startup, run at generate time |
//servo: directive must be the doc comment of a top-level function |
A floating directive registers nothing |
//servo: directives need a servo version that exports the servo.Response family |
The CLI is newer than the module’s servo/v3 |
The handler it documents:
| Message | Cause |
|---|---|
handlers must be top-level functions, not methods |
Directive on a method |
handler app.order must be exported |
The generated server has to call it from the injector package |
handler app.Order must not be generic / must not be variadic |
Same rules as providers |
first parameter must be context.Context |
The request’s context is the contract |
must return (R, error) where R is a servo response type (servo.Json[T], servo.Xml[T] or servo.Response) |
Any other result shape, including a concrete error type |
route GET /ping is declared in package main (…), which no other package can import, and that package has no servo.Build(...) to serve it |
A handler beside a main.go with no injector: only an injector in that same package can call it, and every other injector skips it |
… and that package's injector declares no servo.HTTP() |
The same, with an injector there that serves nothing |
The request struct and its pattern:
| Message | Cause |
|---|---|
request struct type app.orderReq must be exported |
The generated server constructs one |
field X … must be a scalar (string, bool, int/uint families, float32/64) |
A bound field the generated strconv parsing can’t decode |
field X … has more than one binding tag |
A field binds from exactly one source |
field x … is unexported but carries a binding tag |
The decoder cannot assign it |
fields Page and P … both bind query "page" |
Last-write-wins would be silent |
request struct … mixes form fields with JSON body fields |
A body is a form or JSON, not both |
request struct … declares JSON body fields, but GET requests carry no body |
Body/form fields on GET/HEAD/DELETE/OPTIONS |
field Category … binds path "category", but the pattern "/order" has no {category} segment |
Tag without a segment reads empty forever |
pattern segment {category} … has no path:"category" field |
Routing data the handler never sees |
Groups, middleware and extractors:
| Message | Cause |
|---|---|
group "grp" must match [A-Za-z0-9_-]+ |
A directive’s trailing token failing the group grammar |
//servo:post takes a pattern and an optional group — unexpected … |
A fourth directive token |
group "backoffice" is not declared by any injector |
A group token no spec’s servo.Group declares — module-wide, so a typo cannot silently strand a route |
T must have a method Middleware(next http.Handler) http.Handler |
A servo.Use type without the wrap method (or the wrong shape) |
servo.Use[T] selects group "x", but this injector serves only: … |
A Group selector naming an unserved group |
servo.Use[T]'s selector "POST /p" matches no served route |
A Route selector matching nothing |
T must have a method Extract(r *http.Request) (T, error) |
A servo.Extract type without the extractor method (or the wrong shape) |
two extractors produce *auth.User |
A produced type maps to exactly one extractor |
*auth.User has both a provider (…) and an extractor (…) — … never both |
A type cannot be both constructed once and extracted per request |
T is scoped, but servo.Use wraps a singleton server / servo.Extract needs a singleton |
Middleware and extractors are resolved once |
http: group "telemetry" declared in the spec but missing from HTTPConfig.Groups |
Runtime (New) — group names are generate-time facts, port numbers are runtime values |
The route set and the graph:
| Message | Cause |
|---|---|
duplicate route POST /order — first declared at … |
Exact group+method+pattern collision, both positions named; the same route on two different groups is two servers and is legal |
route … cannot be registered on net/http.ServeMux: … conflicts with pattern … |
A wildcard overlap net/http itself refuses |
no provider for *….servo.HTTPConfig + needed by HTTP server (servo.HTTP()) |
servo.HTTP() declared, no config provider |
no provider for *app.Missing + needed by handler app.Order (POST /order/…) |
An unresolvable handler dependency — with a hint about binding tags when the type looks like a forgotten request struct |
*chat.Session is scoped, but handler app.Me (GET /me) receives it from the HTTP server |
The widening rule, with the emitted server as the capturing singleton; depend on the accessor instead |
Tracing
servo.Trace adds declaration-time checks — the type argument must be a named,
non-empty interface the injector package can implement, and Expected must name an exported
package-level error — and three resolution-time ones: a traced interface nothing depends on
(same shape as an unused supplied value, checked per generated file), a
module in which nothing imports the OpenTelemetry trace API, and a missing
trace.TracerProvider provider, which replaces the generic no-provider message with one that
says the constructor is yours to write:
cmd/api/spec.go:21:3: servo: no provider for go.opentelemetry.io/otel/trace.TracerProvider
needed by the servo.Trace wrapper for example.com/app/store.Store cmd/api/spec.go:21:3
needed by *example.com/app/api.Server api/api.go:15:6
root cmd/api/spec.go:12:3
Every servo.Trace wrapper takes the tracer provider as an ordinary dependency —
there is no global fallback. Write a constructor whose result implements it;
the SDK's *sdktrace.TracerProvider does, and so does a type of your own that
embeds it and adds the Stop servo will call at shutdown:
...
A fourth refuses, for this release, a wrapper around a scoped implementation. The full table, with causes and fixes, is on Traced interfaces.
Emission
emit: generated source failed to format (this is a servo bug, not a user error):
<the go/format error>
---
<the unformatted source>
Servo emitted source that gofmt couldn’t parse. As the message says, that is a bug in servo, not
in your code. The full unformatted output is included so it can go straight into an
issue.
Commands
generate
servo: the generated code does not compile. It was written so it can be inspected, but this is a
bug in servo rather than in your code — please report it at https://github.com/okian/servo/issues
with these lines:
cmd/app/servo_gen.go:93:19: user.Me undefined (type *user.User has no field or method Me)
After writing, generate type-checks every file it wrote under the build configuration the
generated files are gated for. A compile error inside one of them is reported with its position
and fails the run; the file stays on disk, because the broken output is what a bug report needs.
Errors elsewhere — a main.go the generator has nothing to do with — are not reported here, and a
package whose imports go list cannot resolve yet (the first generated file to import
errgroup or a config decoder, before go mod tidy) is skipped rather than judged.
servo generate: removed cmd/app/servo_gen_test.go — the spec no longer declares servo.Override
Not an error. The override variant is written only while the spec declares a servo.Override;
once the last one goes, nothing would ever regenerate the file, so generate removes it and says
so on stderr. Only a file carrying servo’s own generated-code header is ever touched.
check
servo check: cmd/api/servo_gen.go is stale — run `servo generate`
--- cmd/api/servo_gen.go (committed)
+++ cmd/api/servo_gen.go (fresh)
- server := api.New(db)
+ server := api.New(db, cache)
note: this is servo v3.2.1. If regenerating does not settle it, the machine that
committed the file was running a different version — compare `servo version`, and
pin one for everybody with `go get -tool github.com/okian/servo/v3/cmd/servo`.
The committed file doesn’t match a fresh generation, with a +/- diff. Run servo generate and
commit the result.
The trailing note is on every stale report, because “stale” has one other cause the diff cannot
distinguish: two machines on two servo versions produce a real difference in a file neither of them
edited. If check fails in CI but passes locally, that is the usual reason — compare the two
servo versions and pin one with go get -tool, which is what the tool directive in go.mod is
for.
servo check: cmd/api/servo_gen.go does not exist — run `servo generate`
The file was never generated, or was deleted. Same fix, plus commit it — see
doctor.
Both messages cover servo_gen_test.go too, when the spec declares a servo.Override: the
override variant is compared the same way. And in the other direction:
servo check: cmd/api/servo_gen_test.go is left over from a spec that no longer declares
servo.Override — run `servo generate`, which removes it
For a variant, both messages name the flags that would actually produce
that file — run `servo generate --tags=prod` — since a bare servo generate would regenerate
the default variant and leave the missing one missing.
doctor
servo doctor: problems found
Printed after the report when any line was [FAIL]. Read the report — each line names its own
problem. [WARN] and [INFO] lines never cause this: WARN is a check that could not reach a
verdict (no git, no repo), and INFO is the variant inventory, which reports rather than judges.
servo doctor:
[FAIL] servo.prod_gen.go is generated from a spec that no longer exists — delete it, or
restore the spec file it came from
The one doctor check nothing else makes. A generated file whose spec was deleted still compiles
into whichever build satisfies its constraint, and nothing will ever regenerate it, so it drifts
from the moment the spec went away.
explain and why
| Message | Cause |
|---|---|
servo: no node matches "X" |
No exact match and no type string ending with X. A leading * never suffix-matches: write api.Server, not *api.Server |
servo: "X" matches multiple nodes, be more specific: … |
Ambiguous suffix. The candidates are listed; pick one |
servo why: X is not reachable from any root |
The node resolved but no root — and no servo.HTTP() handler, middleware or extractor, which why searches from next — depends on it |
usage: servo explain [--json] <type> |
Zero or several positional arguments. Flags must come before the type |
usage: servo why [--json] <type> |
The same, for why |
Others
| Message | Cause |
|---|---|
servo: unknown command "X" |
Typo, or a flag placed before the command name. The full command list is printed underneath it, and again by servo help |
servo graph: unknown --format "X" (want text\|json\|dot\|mermaid) |
Unsupported format |
servo init: <path> already exists |
init never overwrites the spec file |
servo new: unknown kind "X" (want component\|adapter\|mock-adapter) |
Typo in the scaffold kind |
servo new mock-adapter: unknown tool "X" (want moq\|mockery\|gomock) |
Only those three are scaffolded |
flag provided but not defined: -X |
Standard flag package error — check the flag belongs to that command |
servo-vet
spec.go:9:2: servo: servo.Build called in a file without a `//go:build servoinject` constraint —
it will compile into the real binary and panic at runtime; run `servo init` or add the tag
chat/chat.go:91:6: servo: ScopeKey must not name its receiver — servo calls it on a typed nil,
so a receiver the body can reach is a nil dereference in production; write
`func (*T) ScopeKey(...)`
Two of the three things servo-vet reports, and two of the mistakes the
compiler cannot catch on its own; the third is a malformed //servo: comment directive, reported
with the same message generate prints for it. The generator makes all three checks too; the
analyzer catches them anywhere, in your editor, before generation runs at all — including in
packages no injector has reached yet.
The first fires for any marker — Build, or any servo function returning Marker, HTTPOption,
ScopeOption or TraceOption, which is how a marker added later is covered without a change to
the analyzer. Include is the one it earns its keep on, since a shared marker set lives in its
own package, away from any spec file, which is where the tag is easiest to forget.
-tags is refused
$ servo-vet -tags=prod ./...
servo-vet: -tags does not work here — it is go/analysis's own no-op flag, so this run would
silently analyse only the default configuration.
To check a tagged configuration, drive servo-vet through the go command, which does understand
build flags:
go vet -tags=prod -vettool=$(which servo-vet) ./...
Exit code 2, not 1 — this is the tool declining to run, go vet’s own convention, and the one
place a servo binary exits with anything other than 0 or 1.
go/analysis registers a -tags flag on every singlechecker binary and documents it as “no
effect (deprecated)”: the checker builds its own packages.Config with no build flags, so the run
would exit 0 having analysed the default configuration while whoever typed it believes prod was
covered. There is no hook to make it work — the config is internal to x/tools — so it is refused
with the invocation that does work. The refusal reads os.Args directly, so it catches -tags,
--tags, -tags=prod and -tags prod anywhere in the argument list.
Runtime reports are not diagnostics
One distinction worth keeping clear. Everything above happens at build time. At run time, servo
reports rather than diagnoses: Shutdown returns a servo.Report, and a
line like
*api.Server: abandoned: context deadline exceeded
is not a servo error — it’s servo telling you a component of yours didn’t stop within its budget. Lifecycle covers what to do about it.