Skip to content

DC feed (collection) endpoints return 500: a bare List is passed to JAXB instead of an Atom feed #247

Closed

Description

@dfcoffin

Summary

First live run of the Data Custodian ESPI API (local profile, valid admin tokens from the AS): every collection ("feed") endpoint that has data returns 500. Authorization succeeds; the failure is in XML marshalling.

ERROR o.g.e.c.service.BaseExportService - Failed to marshal DTO: class java.util.ImmutableCollections$ListN
      nor any of its super class is known to this context.
jakarta.xml.bind.JAXBException: class java.util.ImmutableCollections$ListN nor any of its super class is known to this context.
    at ...common.service.BaseExportService.exportDto(BaseExportService.java:166)
    at ...common.service.impl.ApplicationInformationServiceImpl.export(ApplicationInformationServiceImpl.java:124)
    at ...datacustodian.web.api.ApplicationInformationRESTController.getAllApplicationInformation(...:82)

Root cause

The feed endpoints map entities to a List<XxxDto> (via stream().toList()) and pass the bare list to BaseExportService.exportDto(Object, OutputStream). JAXB can only marshal a root element, so a java.util.List always fails. An ESPI feed must instead be an Atom <feed> (AtomFeedDto) whose <entry> elements carry the resources in <content>, with self/up links per REQ.21.

Affected call sites (13, in 10 controllers under web/api/)

Controller Line(s)
ApplicationInformationRESTController (via ApplicationInformationServiceImpl.export(List, …)) 82
CustomerAccountRESTController 89
CustomerRESTController 89
ElectricPowerQualitySummaryRESTController 95, 164
IntervalBlockRESTController 89, 156
MeterReadingController 94
ReadingTypeRESTController 92
UsagePointController 96, 176
UsageSummaryRESTController 93, 159

Observed live:

  • GET /espi/1_1/resource/ApplicationInformation → 500 with a DataCustodian_Admin_Access or a ThirdParty_Admin_Access token;
  • GET /espi/1_1/resource/UsagePoint → 500 with DataCustodian_Admin_Access.

Single-entry (/{id}) endpoints marshal one DTO and are expected to work; they have not yet been verified live.

Related defect found in the same run

GET /espi/1_1/resource/Authorization returns 200 with an empty body and no Content-Type: AuthorizationController.getAllAuthorizations and getAuthorization are stubs that return null. An empty feed should be a valid Atom <feed> with no entries, and a missing entry should be a 404.

Why tests did not catch it

AbstractControllerMockTest mocks every *ExportService (@MockitoBean UsageExportService, ApplicationInformationExportService, …), so no controller test has ever marshalled a response. DevXmlSampleGenerator exports single entries only.

Proposed fix

  1. Feeds: wrap collections in an Atom feed (feed id/title/updated/self link plus one <entry> per resource) instead of passing a List to JAXB. Do this once, in a shared helper, rather than in 13 places. E1: DC resource engine + Atom entry-assembly (GET-first) — UsagePoint & Customer verticals, scope-enforced #187 (E1 resource engine + Atom entry assembly) replaces these per-resource controllers. Either land E1: DC resource engine + Atom entry-assembly (GET-first) — UsagePoint & Customer verticals, scope-enforced #187 for the affected resources, or add an interim exportFeed(List<?>, …) in BaseExportService that the current controllers call until E1: DC resource engine + Atom entry-assembly (GET-first) — UsagePoint & Customer verticals, scope-enforced #187 retires them.
  2. Authorization: implement the two stub methods, or return an empty feed and a 404 until they are implemented.
  3. Tests: at least one MockMvc test per feed endpoint that uses the real export services with sample data, and validates the body against schema/ESPI_4.0/espi.xsd and atom.xsd (now packaged in OpenESPI-Common, feat(ui): shared Green Button Alliance theme and Third Party layout (prototype) #244). Also one test for an empty feed.
  4. Diagnostics: EspiXmlCaptureFilter (local/dev profiles, dev-xml-capture/run-*/) captures only non-empty XML bodies. A marshalling failure throws before anything is written, so failures leave no trace on disk. Extend it so that a failed /espi/1_1/resource/** request writes GET_<path>.error.txt, containing the status, the exception chain and the DTO type that failed, next to the captured XML. In dev profiles only, also return an RFC 9457 application/problem+json body naming the failing type instead of a bare 500.

Reproduce

TOKEN=$(curl -s -u data_custodian_admin:dc-secret -d grant_type=client_credentials \
  -d scope=DataCustodian_Admin_Access http://localhost:9999/oauth2/token | python -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8081/DataCustodian/espi/1_1/resource/ApplicationInformation   # 500
curl -i -H "Authorization: Bearer $TOKEN" http://localhost:8081/DataCustodian/espi/1_1/resource/Authorization            # 200, empty body

(AS, DC and TP on the local profile, with the DC local client secret dc-secret from #240.)

References

#187 (E1 resource engine), #119 (canonical build plan, EspiXmlCaptureFilter), #141 (TP Authorization stubs), #197 (TP cannot unmarshal Atom feeds: the receiving side of the same format)

🤖 Generated with Claude Code

Activity

  1. added 2 commits that reference this issue on Nov 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions