Parallelize invoice image fetch, add telemetry, generalize HtmlConverter
- InvoiceService.ResolveInlineImagesAsync now fetches an invoice's distinct whitelisted images concurrently (SemaphoreSlim-capped at 4) instead of one at a time. It ran inline on every invoice render, including repeated draft-preview renders, so N sequential mfr-portal round-trips could otherwise add N x latency to a user-facing request. - Add fuchs.invoice.inlineimages.fetches (counter, tagged by outcome: success/http_error/bad_content_type/oversized/exception) and fuchs.invoice.inlineimages.resolve.duration (histogram) to FuchsTelemetry, matching the project's observability convention for outbound-HTTP operations. - MigraDocExtensions.HtmlConverter's pre-resolved-image parameter is renamed inlineImages -> resolvedImages with generic docs: the converter fetches nothing and knows nothing about invoices — which images to resolve, and any whitelist/network policy, is entirely the caller's concern (InvoiceService, ADR 0018 today; any future caller tomorrow). - Add tests asserting fetches actually overlap (not just faster) and that the new counter/histogram are recorded. - Update ADR 0018 to describe the concurrent fetch, telemetry, and the generalized converter parameter. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -44,15 +44,21 @@ so a naive "download whatever `src` says" would let it reach internal/private ne
|
||||
same library the `HtmlConverter` itself parses the identical text with — rather than a regex, so
|
||||
a decoy attribute next to the real `src` (e.g. `data-src="..."`) can't be picked up in its place.
|
||||
Each fetched image is capped at 5MB, must report an `image/*` content-type, and the invoice
|
||||
fetches at most 20 distinct images; failures (unreachable, wrong type, oversized) are logged and
|
||||
just leave that one image out — they never fail the whole render.
|
||||
fetches at most 20 distinct images; failures (unreachable, wrong type, oversized) are logged,
|
||||
counted (`FuchsTelemetry.InvoiceInlineImageFetches`, tagged by outcome), and just leave that one
|
||||
image out — they never fail the whole render. The distinct URLs fetch concurrently (capped at 4
|
||||
in flight via a `SemaphoreSlim`, not one-at-a-time) since this runs inline on every render,
|
||||
including repeated draft-preview renders — total resolve time is recorded in
|
||||
`FuchsTelemetry.InvoiceInlineImagesResolveDuration`.
|
||||
- The resolved `{src → "base64:..."}` map flows down as a plain `IReadOnlyDictionary<string,string>?`
|
||||
parameter through `IPdfService.ApplyInvoice` → `FuchsPdf.ApplyInvoice` → `Cell.AddHtml` into the
|
||||
`HtmlConverter`, whose new `img` handler only emits an image for a `src` present in that map —
|
||||
it does no fetching itself and never sees the network. Image sizing mirrors the HTML: the
|
||||
handler reads `width`/`height` (attribute or inline `style`, as TinyMCE's resize handles write
|
||||
them) and converts CSS px → cm, capped to 7×10cm so a malformed/huge size can't blow out the
|
||||
invoice table layout.
|
||||
it does no fetching itself and never sees the network. The converter itself stays generic (its
|
||||
constructor parameter is named `resolvedImages`, not invoice-specific) — resolving *which* images
|
||||
go in the map, and any network/whitelist policy, is entirely InvoiceService's concern, not the
|
||||
converter's. Image sizing mirrors the HTML: the handler reads `width`/`height` (attribute or
|
||||
inline `style`, as TinyMCE's resize handles write them) and converts CSS px → cm, capped to
|
||||
7×10cm so a malformed/huge size can't blow out the invoice table layout.
|
||||
|
||||
## Consequences
|
||||
- Any future host serving invoice-editor images must be added to the whitelist deliberately (a
|
||||
|
||||
Reference in New Issue
Block a user