Unauthenticated GET /api/v1/supply-chain returns HTTP 500 when SELLER_ORGANIZATION_ID is not configured. The route declares only a 200 response in the served OpenAPI.
The route's default branch — reached when SELLERS_JSON_PATH is absent, and documented as returning a default single-node chain — constructs SupplyChainNodeModel with sid=seller_id at src/ad_seller/interfaces/api/routers/admin.py:278, where sid is a required str. seller_id is read as getattr(settings, "seller_organization_id", "default"). seller_organization_id is declared Optional[str] = None at src/ad_seller/config/settings.py:113, so the attribute exists and the three-argument fallback never fires. The value passed is None and pydantic raises ValidationError: Input should be a valid string.
The two sibling values read on the same call show the distinction: seller_domain and seller_name use the identical getattr idiom but are not attributes of the settings object, so their fallbacks do fire and return demo-publisher.example.com and Demo Publisher. With SELLER_ORGANIZATION_ID configured, the endpoint returns 200 and those two fallback values appear alongside the configured seller_id in a single response.
Observed on v2.4.2 (e5b367d) in WSL2 Ubuntu 24.04 and a clean Ubuntu 24.04 container, the container run from a freshly initialised datastore. The script below was run unchanged in both environments.
The script below is the exact file run in both environments:
sha256 fd6fd8890a6fb26f7038d65d7b443b12e882c810d66b3a758728c22bd7658e76
No credentials are required. Because a configured SELLER_ORGANIZATION_ID yields a 200 that is indistinguishable from the defect being absent, the script reports an unmet precondition separately from a non-reproduction: on a 200 carrying a seller_id it reports the configured condition rather than a negative result.
Reproduction
#!/usr/bin/env bash
# M reproduction — GET /api/v1/supply-chain returns 500 with default configuration
#
# Published reproduction. Prints what the server does and summarises whether the
# behaviour reproduced. Always exits 0: a build that does NOT reproduce is a valid
# and useful result, not a script failure.
#
# The runtime evidence here is the symptom. The wire cannot carry the cause — the
# 500 body is opaque — so this script establishes that the failure is specific to
# this endpoint rather than a general server fault, and shows the published schema
# the endpoint fails to satisfy. Set SELLER_LOG to a readable server log to also
# print the validation error.
#
# Usage: [BASE=host:port] [SELLER_LOG=/path/to/server.log] bash repro-m.sh
# PRECONDITION: the seller must be running with SELLER_ORGANIZATION_ID unset, so
# that settings.seller_organization_id is None. It is declared
# `Optional[str] = None`, so the three-argument getattr fallback in the route
# never fires for it. If the value is configured, sid is a valid string and the
# endpoint returns 200 — that is the precondition being absent, NOT the defect
# being fixed, and this script reports the two differently.
#
# pydantic-settings reads .env from the server's working directory, so the
# cleanest way to get the unset condition without editing your .env is to launch
# from a directory that has none:
# mkdir -p /tmp/m-noenv && cd /tmp/m-noenv
# uv run --project /path/to/seller-agent uvicorn \
# ad_seller.interfaces.api.main:app --host 127.0.0.1 --port 8002
#
# No credentials needed — this endpoint is unauthenticated.
# Creates: nothing.
B="${BASE:-localhost:8001}"
# Clear scratch files from any previous run: a failed fetch would otherwise leave
# a stale file in place and let a check pass on last run's data.
rm -f /tmp/m-*.json
echo "-- 1. GET /api/v1/supply-chain, unauthenticated, default configuration --"
SC_CODE=$(curl -sS "$B/api/v1/supply-chain" -o /tmp/m-sc.json -w "%{http_code}")
export SC_CODE
echo "HTTP $SC_CODE"
echo " body: $(head -c 120 /tmp/m-sc.json)"
if [ "$SC_CODE" = "200" ]; then
echo " -- endpoint answered; reading back the configured seller_id --"
python3 -c "
import json
try: d=json.load(open('/tmp/m-sc.json'))
except Exception: raise SystemExit(' seller_id: <unreadable body>')
print(' seller_id:',repr(d.get('seller_id')))" || true
fi
echo
echo "-- 2. control: the server is healthy and the failure is endpoint-specific --"
P_CODE=$(curl -sS "$B/products" -o /tmp/m-products.json -w "%{http_code}")
export P_CODE
echo " GET /products HTTP $P_CODE"
python3 -c "
import json
d=json.load(open('/tmp/m-products.json')); p=d.get('products',d)
print(' catalogue size:',len(p) if isinstance(p,list) else 'n/a')" 2>/dev/null || echo " catalogue: unreadable"
echo
echo "-- 3. the route and response contract the server itself publishes --"
curl -fsS "$B/openapi.json" -o /tmp/m-openapi.json || echo " (openapi fetch failed)"
python3 -c "
import json,sys
try: d=json.load(open('/tmp/m-openapi.json'))
except Exception: print(' openapi: unavailable'); sys.exit(0)
paths=d.get('paths',{})
print(' /api/v1/supply-chain declared:', '/api/v1/supply-chain' in paths)
if '/api/v1/supply-chain' in paths:
print(' declared responses:', sorted((paths['/api/v1/supply-chain'].get('get') or {}).get('responses',{}).keys()))
schemas=d.get('components',{}).get('schemas',{})
node=schemas.get('SupplyChainNodeModel') or {}
print(' SupplyChainNodeModel required:', node.get('required'))
sid=(node.get('properties') or {}).get('sid') or {}
print(' sid declared type:', sid.get('type') or sid)"
if [ -n "${SELLER_LOG:-}" ] && [ -r "${SELLER_LOG}" ]; then
echo
echo "-- 3b. validation error from SELLER_LOG (optional, not a summary check) --"
grep -A3 'SupplyChainNodeModel' "$SELLER_LOG" | tail -6 | sed 's/^/ /'
fi
echo
echo "-- summary --"
python3 - <<'PYEOF'
import json, os
def _load(path):
# A failed fetch must degrade to an unchecked box, never a traceback.
try:
return json.load(open(path))
except Exception:
return {}
oa = _load('/tmp/m-openapi.json')
paths = oa.get('paths', {})
schemas = oa.get('components', {}).get('schemas', {})
node = schemas.get('SupplyChainNodeModel') or {}
sid = (node.get('properties') or {}).get('sid') or {}
code = os.environ.get('SC_CODE')
# Precondition gate. A 200 carrying a real seller_id means SELLER_ORGANIZATION_ID
# was configured, so the defect was never exercised. Reporting that as "NO" would
# be an underdetermined result: it cannot be told apart from a genuine fix.
configured = None
if code == '200':
configured = _load('/tmp/m-sc.json').get('seller_id')
checks = [
("GET /api/v1/supply-chain returned HTTP 500", code == '500'),
("the route is declared in the served OpenAPI", '/api/v1/supply-chain' in paths),
("SupplyChainNodeModel requires sid", 'sid' in (node.get('required') or [])),
("sid is declared as a string", sid.get('type') == 'string'),
("control: GET /products returned HTTP 200", os.environ.get('P_CODE') == '200'),
]
for label, ok in checks:
print(f" [{'x' if ok else ' '}] {label}")
print()
if configured:
print(f" PRECONDITION NOT MET: seller_id is {configured!r}, so")
print(" SELLER_ORGANIZATION_ID was configured and the defect was never exercised.")
print(" Relaunch with it unset (see the header) and run again.")
print()
print(" REPRODUCED: not tested — precondition not met")
else:
print(" REPRODUCED:", "yes" if all(v for _, v in checks) else "NO — see unchecked boxes above")
PYEOF
exit 0
Observed
GET /api/v1/supply-chain returns HTTP 500 with an empty body. GET /products returns HTTP 200 in the same run, so the failure is specific to this route rather than a general server fault. The served OpenAPI declares the route, declares only a 200 response for it, and declares SupplyChainNodeModel.sid as a required string. Where the server log is readable, it records ValidationError: 1 validation error for SupplyChainNodeModel / sid / Input should be a valid string [input_value=None].
Not tested: whether a configured SELLERS_JSON_PATH whose primary entry carries no seller id reaches the same model construction on the other branch at admin.py:265.
Unauthenticated
GET /api/v1/supply-chainreturns HTTP 500 whenSELLER_ORGANIZATION_IDis not configured. The route declares only a 200 response in the served OpenAPI.The route's default branch — reached when
SELLERS_JSON_PATHis absent, and documented as returning a default single-node chain — constructsSupplyChainNodeModelwithsid=seller_idatsrc/ad_seller/interfaces/api/routers/admin.py:278, wheresidis a requiredstr.seller_idis read asgetattr(settings, "seller_organization_id", "default").seller_organization_idis declaredOptional[str] = Noneatsrc/ad_seller/config/settings.py:113, so the attribute exists and the three-argument fallback never fires. The value passed isNoneand pydantic raisesValidationError: Input should be a valid string.The two sibling values read on the same call show the distinction:
seller_domainandseller_nameuse the identicalgetattridiom but are not attributes of the settings object, so their fallbacks do fire and returndemo-publisher.example.comandDemo Publisher. WithSELLER_ORGANIZATION_IDconfigured, the endpoint returns 200 and those two fallback values appear alongside the configuredseller_idin a single response.Observed on v2.4.2 (
e5b367d) in WSL2 Ubuntu 24.04 and a clean Ubuntu 24.04 container, the container run from a freshly initialised datastore. The script below was run unchanged in both environments.The script below is the exact file run in both environments:
No credentials are required. Because a configured
SELLER_ORGANIZATION_IDyields a 200 that is indistinguishable from the defect being absent, the script reports an unmet precondition separately from a non-reproduction: on a 200 carrying aseller_idit reports the configured condition rather than a negative result.Reproduction
Observed
GET /api/v1/supply-chainreturns HTTP 500 with an empty body.GET /productsreturns HTTP 200 in the same run, so the failure is specific to this route rather than a general server fault. The served OpenAPI declares the route, declares only a 200 response for it, and declaresSupplyChainNodeModel.sidas a required string. Where the server log is readable, it recordsValidationError: 1 validation error for SupplyChainNodeModel / sid / Input should be a valid string [input_value=None].Not tested: whether a configured
SELLERS_JSON_PATHwhose primary entry carries no seller id reaches the same model construction on the other branch atadmin.py:265.