Skip to content

Error flattening in table/ package: all DB errors wrapped as NotFound #120

Description

@dev-arya23

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 NotFoundcodes.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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions