Summary
The table/ package (generic.go and cached_generic.go) wraps all errors from FindOne and FindMany operations as errors.NotFound, regardless of the actual error type. This destroys error semantics for downstream consumers that need to distinguish between:
- Permanent not-found (document doesn't exist) → should map to
codes.NotFound
- Transient failures (network blip, deadline exceeded, primary stepdown) → should map to
codes.Unavailable and be retried
Context
Surfaced during review of agentic-core/oauth-connection-manager PR #36, where OCM's classifyLookup maps NotFound → codes.NotFound and other errors → codes.Unavailable. Because the table/ layer flattens everything to NotFound, transient Mongo errors are misclassified — adapters receiving codes.NotFound for a transient DB error won't retry and will treat it as a missing integration.
Affected Methods
All six find methods in the table/ package have this pattern:
table/generic.go
| Method |
Line Pattern |
Issue |
Find() |
errors.Wrapf(errors.NotFound, "failed to find entry with key %v: %s", key, err) |
Wraps any FindOne error as NotFound |
FindMany() |
errors.Wrapf(errors.NotFound, "failed to find any entry: %s", err) |
Wraps any FindMany error as NotFound |
FindManyWithOpts() |
errors.Wrapf(errors.NotFound, "failed to find any entry: %s", err) |
Wraps any FindMany error as NotFound |
table/cached_generic.go
| Method |
Line Pattern |
Issue |
DBFind() |
errors.Wrapf(errors.NotFound, "failed to find entry with key %v: %s", key, err) |
Wraps any FindOne error as NotFound |
DBFindMany() |
errors.Wrapf(errors.NotFound, "failed to find any entry: %s", err) |
Wraps any FindMany error as NotFound |
DBFindManyWithOpts() |
errors.Wrapf(errors.NotFound, "failed to find any entry: %s", err) |
Wraps any FindMany error as NotFound |
Root Cause
The lower layer db/mongo.go already has correct error discrimination via interpretMongoError():
func interpretMongoError(err error) error {
if mongo.IsDuplicateKeyError(err) {
return errors.Wrap(errors.AlreadyExists, err.Error())
}
if err == mongo.ErrNoDocuments {
return errors.Wrap(errors.NotFound, err.Error())
}
return err
}
However, the table/ layer re-wraps the result as NotFound unconditionally, destroying the error semantics that interpretMongoError() correctly preserves.
Recommended Fix
For each affected method, check whether the error from the db layer is already NotFound before wrapping:
For FindOne-based methods (Find, DBFind):
err := t.col.FindOne(ctx, key, &data)
if err != nil {
if errors.IsNotFound(err) {
return nil, errors.Wrapf(errors.NotFound, "failed to find entry with key %v: %s", key, err)
}
return nil, errors.Wrapf(errors.Internal, "failed to find entry with key %v: %s", key, err)
}
For FindMany-based methods (FindMany, FindManyWithOpts, DBFindMany, DBFindManyWithOpts):
err := t.col.FindMany(ctx, filter, &data, opts)
if err != nil {
if errors.IsNotFound(err) {
return nil, errors.Wrapf(errors.NotFound, "failed to find any entry: %s", err)
}
return nil, errors.Wrapf(errors.Internal, "failed to find any entry: %s", err)
}
This preserves the NotFound semantics for genuine not-found results while letting transient errors propagate with their original error code (or Internal), so downstream consumers can correctly distinguish retriable from permanent failures.
Impact
- Current blast radius: Narrow — primarily affects cold/bind-path integration lookups in OCM. But the pattern is in a shared library used by multiple services.
- Priority: P2 — not blocking any active PR, but should be addressed before more services adopt
Table.Find/CachedTable.DBFind for authoritative reads where error classification matters.
Additional Notes
Other uses of errors.NotFound in the repo are correct:
rate/limit-manager.go — wraps in-memory map lookup miss (not a DB error)
utils/object-encryptor.go — wraps in-memory map lookup miss
certmanager/authority.go — wraps in-memory map lookup miss
db/mongo.go interpretMongoError() — correctly checks mongo.ErrNoDocuments specifically
db/mongo.go UpdateOne()/DeleteOne()/DeleteMany() — correctly wrap zero-match results as NotFound
Summary
The
table/package (generic.goandcached_generic.go) wraps all errors fromFindOneandFindManyoperations aserrors.NotFound, regardless of the actual error type. This destroys error semantics for downstream consumers that need to distinguish between:codes.NotFoundcodes.Unavailableand be retriedContext
Surfaced during review of
agentic-core/oauth-connection-managerPR #36, where OCM'sclassifyLookupmapsNotFound→codes.NotFoundand other errors →codes.Unavailable. Because thetable/layer flattens everything toNotFound, transient Mongo errors are misclassified — adapters receivingcodes.NotFoundfor a transient DB error won't retry and will treat it as a missing integration.Affected Methods
All six find methods in the
table/package have this pattern:table/generic.goFind()errors.Wrapf(errors.NotFound, "failed to find entry with key %v: %s", key, err)FindOneerror asNotFoundFindMany()errors.Wrapf(errors.NotFound, "failed to find any entry: %s", err)FindManyerror asNotFoundFindManyWithOpts()errors.Wrapf(errors.NotFound, "failed to find any entry: %s", err)FindManyerror asNotFoundtable/cached_generic.goDBFind()errors.Wrapf(errors.NotFound, "failed to find entry with key %v: %s", key, err)FindOneerror asNotFoundDBFindMany()errors.Wrapf(errors.NotFound, "failed to find any entry: %s", err)FindManyerror asNotFoundDBFindManyWithOpts()errors.Wrapf(errors.NotFound, "failed to find any entry: %s", err)FindManyerror asNotFoundRoot Cause
The lower layer
db/mongo.goalready has correct error discrimination viainterpretMongoError():However, the
table/layer re-wraps the result asNotFoundunconditionally, destroying the error semantics thatinterpretMongoError()correctly preserves.Recommended Fix
For each affected method, check whether the error from the
dblayer is alreadyNotFoundbefore wrapping:For
FindOne-based methods (Find,DBFind):For
FindMany-based methods (FindMany,FindManyWithOpts,DBFindMany,DBFindManyWithOpts):This preserves the
NotFoundsemantics for genuine not-found results while letting transient errors propagate with their original error code (orInternal), so downstream consumers can correctly distinguish retriable from permanent failures.Impact
Table.Find/CachedTable.DBFindfor authoritative reads where error classification matters.Additional Notes
Other uses of
errors.NotFoundin the repo are correct:rate/limit-manager.go— wraps in-memory map lookup miss (not a DB error)utils/object-encryptor.go— wraps in-memory map lookup misscertmanager/authority.go— wraps in-memory map lookup missdb/mongo.gointerpretMongoError()— correctly checksmongo.ErrNoDocumentsspecificallydb/mongo.goUpdateOne()/DeleteOne()/DeleteMany()— correctly wrap zero-match results asNotFound