Back to blog
SecurityFor developers6 min read

What We Learned Building a Resilient Path to France's Public Legal Data

Our legal research agent started answering confidently with zero citations. The cause wasn't AI — it was a single external dependency becoming unreachable. Here's the resilience principle we applied, and the infrastructure we built around it.

Jérémie EscachCTOPublished on July 16, 2026

Last week, our legal research agent started giving lawyers confident, well-structured answers to their questions — with zero citations attached. No case references, no article numbers, nothing. For a legal research tool, an unsourced answer isn't just unhelpful, it's the one failure mode we can't tolerate.

The cause wasn't the AI model, and it wasn't a prompt regression. It was three layers down, in the network path to an external dependency — and it taught us something worth writing up properly.

What we build on

Our Legal Research and Legal Watch agents are grounded in official French legal data: Légifrance for statutes, codes, and the case-law index, and Judilibre for Cour de cassation decisions. Both are public infrastructure, maintained for the entire French legal ecosystem, and both are the correct source to build on — using anything less official would mean weaker guarantees on accuracy and freshness, which is not a trade-off a legal research tool can make.

Depending on public infrastructure like this is, on balance, a good decision. It is also, unavoidably, a dependency — and dependencies need to be designed for, not just assumed to always work.

The investigation

The symptom looked like a model problem: an agent producing fluent legal reasoning with no supporting sources. The instinct is to suspect the prompt, the retrieval step, or the model itself. We ruled all three out first — the agent's tool-calling logic was firing correctly, and the same query against a fixture worked fine in a lower environment.

That left the network path. Isolating the actual cause took a few careful steps:

  1. Credentials first. Confirmed the API credentials were valid, current, and correctly scoped — production credentials, tested directly against the OAuth endpoint on PISTE, the gateway both Légifrance and Judilibre sit behind. They worked immediately when tested from an unrelated network.
  2. Same credentials, different origin, different result. That one data point ruled out anything about the credentials themselves. Whatever was failing was tied to where the request came from, not what it contained.
  3. Reading the response headers, not just the status code. The failing requests came back 403, but with x-iplb-* headers attached — the fingerprint of an OVHcloud load balancer sitting in front of PISTE's API layer. That's the concrete diagnostic signal: it means the rejection happens at the network edge, before the request ever reaches application-level auth logic. A credentials or scope problem looks different — it fails the same way regardless of where it's sent from, and the error is usually more specific than a bare 403.

If you're integrating with PISTE and hit a 403 that doesn't correlate with anything wrong in your request, checking for those headers is the fastest way to confirm you're looking at IP-level access control, not a bug in your own auth code. We didn't find this documented anywhere obvious going in — worth knowing before you point production traffic at it.

None of this pointed to anything wrong with our code, our credentials, or the underlying data itself — it was specific to the network path our production traffic happened to take.

The principle, not just the fix

It's tempting to treat this as a one-off incident with a one-off fix. We don't think that's the right lesson. The actual lesson is more general, and it applies to any external dependency, not just this one:

Any single path to any single provider — however official, however reliable in general — can become unavailable to you specifically, for reasons entirely outside your application code. Network routing changes, edge infrastructure behaves differently for different origins, rate limits apply asymmetrically, regional infrastructure has regional quirks. None of that is a defect in the provider. It's just what happens when a request has to cross real infrastructure between two real systems.

The design implication: a hard dependency on one path to reach a critical provider is a single point of failure, even if the provider itself has excellent uptime. If your product's behavior degrades silently the moment that one path has a bad day, you haven't built resilience — you've built a feature that happens to work most of the time.

We apply this principle broadly across the platform: LLM calls go through a provider-neutral adapter layer specifically so no single model provider is a hard dependency; checkpointing is pluggable across storage backends for the same reason. This incident was a reminder to apply the same standard to external data APIs, not just to the components we control end-to-end.

The fix — a second path, not a workaround

The fix is a dedicated, secondary network path to the same official APIs, used only when the primary path is degraded. We built it as a small, isolated relay — a separate lightweight server whose only job is to forward traffic — connected over an encrypted, dedicated tunnel.

Two design constraints mattered more than the plumbing itself:

  • Off by default, additive only. The primary path is unchanged and remains the default for every request. The secondary path exists purely as a fallback, gated behind an explicit configuration flag. Nothing about normal operation changes.
  • Zero content visibility, by construction. The relay operates at the transport level only. TLS is established end-to-end between our servers and the legal-data provider — the relay never terminates it, never inspects it, and never could. The only thing that ever crosses that relay is encrypted traffic; the only thing an operator of that relay could ever observe is connection metadata (timing, byte counts, IP/port pairs) — never a query, a token, or a single byte of client data. We treated this the same way we treat every other piece of infrastructure that touches, even indirectly, a request originating from a client document: minimize what it can see, by design, not by policy.

We validated the fix the same way we validate anything before calling it done: reproduce the failure on the primary path, confirm success on the secondary path, and confirm — end to end, inside the actual running service — that the whole chain works, not just the isolated network hop.

What we take away from this

The interesting failure here was never "an API had a bad day." APIs have bad days; that's not news. The interesting failure was that our own architecture had exactly one way to reach a critical dependency, and no fallback when that one way degraded. That's a design gap, and design gaps are fixable in a way that's actually satisfying: not by hoping the dependency behaves better next time, but by making sure your product doesn't care which specific path got the data through.

That's the standard we're holding every external dependency to going forward — not just the ones that have already caused an incident.

Frequently asked questions

Does your legal research agent use official French legal data sources?

Yes. Our Legal Research and Legal Watch agents query Légifrance (statutes, codes, case law index) and Judilibre (Cour de cassation decisions) directly — the official public data sources maintained for the French legal ecosystem. We don't scrape or rely on unofficial mirrors.

Did this incident expose any client data?

No. The connectivity issue affected only our ability to reach an external legal-data API — it never touched client documents, queries, or credentials. The resilience fix we describe here was also designed with the same standard: the added network path can only ever see encrypted connection metadata, never content.

Why not just avoid dependencies on external legal databases entirely?

Because building or maintaining a complete, up-to-date mirror of France's statutes, codes, and case law would be slower, less accurate, and less trustworthy than using the official sources directly. The right answer isn't to avoid the dependency — it's to make sure no single path to it can take your product down.

How do you tell a PISTE IP restriction apart from a credentials problem?

Two signals. First, test the same credentials from a different network — if they succeed there and fail from your production origin, the problem is tied to where the request comes from, not what it contains. Second, look at the response headers on the failing request: PISTE-fronted APIs (Légifrance, Judilibre) sit behind an OVHcloud load balancer, and a 403 accompanied by x-iplb-* headers indicates the request was rejected at that network edge, before it ever reached application-level auth logic. A credentials or scope problem looks different: it fails consistently regardless of origin, and the error is typically more specific than a bare 403.