⚙ Engineer view
Implementation detail, patterns, dependencies, gotchas · Software Engineer, Tech Lead
⚡ Agent trace · 13 steps · $0.0421 · click to expand
search_claims(query='module overview architecture', category='informational') →
search_claims(query='entry point main', category='any') →
search_claims(query='design pattern', category='any') →
search_claims(query='class interface abstract', category='structural') →
search_claims(query='singleton factory strategy', category='any') →
search_claims(category='any', max_results=50) →
search_claims(category='any', max_results=50) →
read_source_file(file_path='fhirclient/__init__.py', start_line=1) →
read_source_file(file_path='fhirclient/client.py', start_line=1) →
read_source_file(file_path='fhirclient/server.py', start_line=1) →
read_source_file(file_path='fhirclient/auth.py', start_line=1) →
read_source_file(file_path='fhirclient/client.py', start_line=62) →
read_source_file(file_path='fhirclient/auth.py', start_line=61) →
read_source_file(file_path='fhirclient/server.py', start_line=62) →
read_source_file(file_path='fhirclient/auth.py', start_line=122) →
read_source_file(file_path='fhirclient/server.py', start_line=120) →
read_source_file(file_path='fhirclient/client.py', start_line=123) →
read_source_file(file_path='fhirclient/client.py', start_line=184) →
read_source_file(file_path='fhirclient/server.py', start_line=180) →
read_source_file(file_path='fhirclient/auth.py', start_line=183) →
read_source_file(file_path='fhirclient/auth.py', start_line=244) →
read_source_file(file_path='fhirclient/server.py', start_line=240) →
read_source_file(file_path='fhirclient/auth.py', start_line=304) →
read_source_file(file_path='fhirclient/server.py', start_line=300) →
read_source_file(file_path='fhirclient/_utils.py', start_line=1) →
read_source_file(file_path='demos/flask/flask_app.py', start_line=1) →
read_source_file(file_path='demos/flask/flaskbeaker.py', start_line=1) →
read_source_file(file_path='demos/flask/flask_app.py', start_line=62) →
read_source_file(file_path='fhirclient/_utils.py', start_line=62) →
read_source_file(file_path='fhirclient/_utils.py', start_line=100) →
read_source_file(file_path='demos/flask/flask_app.py', start_line=122)
Engineer's Guide
Module Map
| Module/File | Role | Key Responsibilities | In-Degree |
|---|---|---|---|
fhirclient/__init__.py |
Package root | Re-exports __version__ from client |
0 (leaf) |
fhirclient/client.py |
Orchestrator | FHIRClient — manages auth flow, patient context, state serialization, delegates to FHIRServer |
Highest (imported by demo, tests) |
fhirclient/server.py |
HTTP & Capability layer | FHIRServer — HTTP GET/PUT/POST/DELETE, CapabilityStatement fetching, auth delegation, error mapping (401→FHIRUnauthorizedException, 403→FHIRPermissionDeniedException, 404→FHIRNotFoundException) |
High (imported by client, tests, models) |
fhirclient/auth.py |
Auth strategy hierarchy | FHIRAuth (base, no-op) + FHIROAuth2Auth (OAuth2 with PKCE, client credentials, refresh tokens). Factory via create() |
Medium (imported by server) |
fhirclient/_utils.py |
Pagination utility | iter_pages(), _fetch_next_page(), _sanitize_next_link() — bundle pagination iterator |
Low (used by models) |
demos/flask/flask_app.py |
Reference Flask app | Full SMART on FHIR launch flow: login, callback, patient display, prescriptions | N/A (demo) |
demos/flask/flaskbeaker.py |
Session adapter | FlaskBeaker(SessionInterface) — bridges Beaker sessions into Flask |
N/A (demo helper) |
tests/client_test.py |
Integration tests | Tests FHIRClient state, patient property, reauthorization |
N/A |
tests/server_test.py |
Server tests | Tests FHIRServer state serialization, uses MockServer |
N/A |
tests/fhirdate_test.py |
Date tests | Tests FHIR date/time types | N/A |
tests/fhirreference_test.py |
Reference tests | Tests bundle reference resolution with MockServer |
N/A |
tests/utils_pagination_test.py |
Pagination tests | Tests iter_pages with MockServer |
N/A |
Design Patterns in Use
1. Strategy Pattern — Auth Hierarchy
File: fhirclient/auth.py:13-30, :145-148
FHIRAuth is a base class with a no-op implementation (auth_type = "none"). FHIROAuth2Auth extends it with auth_type = "oauth2". The ready, authorize_uri, handle_callback, reauthorize, and signed_headers methods all differ between the two. The server delegates to whichever auth instance is attached:
# fhirclient/server.py:116-118
def authorize_uri(self):
if self.auth is None:
self.get_capability()
return self.auth.authorize_uri(self)
2. Factory Method — Auth Creation
File: fhirclient/auth.py:88-97
FHIRAuth.create(auth_type, state) is a classmethod factory that looks up auth_type in FHIRAuth.auth_classes dict and instantiates the registered subclass. Registration happens via the register() classmethod (auth.py:20-30), which subclasses call at class definition time.
3. Registry Pattern — Auth Class Registration
File: fhirclient/auth.py:16-30
FHIRAuth.auth_classes = {} is a class-level dict. Each subclass calls register() to add itself. The factory create() uses this registry. This is a clean, extensible design — adding a new auth type means subclassing FHIRAuth, setting auth_type, and calling register().
4. State Serialization / Memento Pattern
File: fhirclient/client.py:214-226, fhirclient/server.py:301-308, fhirclient/auth.py:133-142
Every layer has a state property returning a dict and a from_state(state) method that restores from that dict. FHIRClient.state nests FHIRServer.state, which nests FHIRAuth.state. The save_func callback (client.py:63) is called after every state mutation, enabling persistence (e.g., Flask session).
5. Delegation / Facade
File: fhirclient/client.py:127-152
FHIRClient is a facade over FHIRServer, which is a facade over FHIRAuth. client.authorize() → server.authorize() → auth.authorize(server). client.handle_callback(url) → server.handle_callback(url) → auth.handle_callback(url, server). This three-layer delegation is consistent throughout.
Critical Implementation Details
1. The base_uri Length Check is a Magic Number
File: fhirclient/server.py:49-58
if base_uri is not None and len(base_uri) > 10:
self.base_uri = base_uri if "/" == base_uri[-1] else base_uri + "/"
self.aud = base_uri
...
if not self.base_uri or len(self.base_uri) <= 10:
raise Exception("FHIRServer must be initialized with `base_uri`...")
The constant 10 is used as a minimum URI length. A URI like http://a.b (12 chars) passes, but https://x.y (13 chars) also passes. This is a heuristic — it works but is fragile. If someone passes "http://x.y" (11 chars) it passes; "http://x.c" (10 chars) fails. The comment says "A URI can't possibly be less than 11 chars" but the code checks > 10, meaning 11+ passes. This is a subtle off-by-one in the comment vs. code.
2. from_state Uses or — Cannot Restore Falsy Values
File: fhirclient/client.py:228-238
self.app_id = state.get("app_id") or self.app_id
self.app_secret = state.get("app_secret") or self.app_secret
self.scope = state.get("scope") or self.scope
This means if app_id is "" or None in the state dict, the existing value is kept. You cannot clear a value by passing an empty string in state — it will be ignored. Same pattern in server.py:313 and auth.py:142. This is a deliberate design choice (state is additive/overriding, not resetting), but it's a gotcha if you expect to clear fields via state.
3. _sanitize_next_link Docstring Lies
File: fhirclient/_utils.py:48-73
The docstring says it "validates hostname against the origin server" but the implementation only checks that scheme is http/https and netloc is non-empty. No hostname comparison happens. This is a documented-but-unimplemented security check. The claim database flags this as [drift:low].
4. PKCE Code Verifier is Generated Once, Never Rotated on Re-authorization
File: fhirclient/auth.py:241-243
if self.code_verifier is None:
self.code_verifier = secrets.token_urlsafe(64)
server.should_save_state()
The code verifier is generated only if None. If the auth state is serialized and restored, the same verifier is reused. This is correct for the authorization code flow (the verifier must match the challenge from the initial authorize request), but if reset() is called (auth.py:173-177), the verifier is cleared and a new one will be generated on the next authorize_uri() call.
5. FHIRServer Uses a Single requests.Session for All Requests
File: fhirclient/server.py:44
self.session = requests.Session()
This means connection pooling, cookie persistence, and default headers are shared across all requests for the lifetime of the FHIRServer instance. This is good for performance but means that if the server sets cookies (unlikely for FHIR but possible), they persist. More importantly, the session is not serialized in state — it's recreated fresh on from_state().
6. patient Property Has a Reauthorization Retry Loop
File: fhirclient/client.py:166-185
try:
self._patient = Patient.read(self.patient_id, self.server)
except FHIRUnauthorizedException:
if self.reauthorize():
self._patient = Patient.read(self.patient_id, self.server)
except FHIRNotFoundException:
self.patient_id = None
If the first read gets a 401, it attempts reauthorization and retries exactly once. If reauthorization fails (returns None), the patient stays None and no further retry happens. The patient_id is only cleared on 404, not on 401. This means a stale patient_id can persist even when the token is expired and reauthorization fails.
7. desired_scope Property Mutates Scope Based on Launch Context
File: fhirclient/client.py:91-101
@property
def desired_scope(self):
scope = self.scope
if self.launch_token is not None:
scope = " ".join([scope_haslaunch, scope])
elif self.patient_id is None and self.wants_patient:
scope = " ".join([scope_patientlaunch, scope])
return scope
This is a property, not a method — it recomputes every time. It prepends "launch" if there's a launch token, or "launch/patient" if no patient is selected and wants_patient is True. This is called during authorize_uri() generation, so the scope sent to the authorization server depends on the current client state.
Entry Points — Start Here
1. fhirclient/client.py — FHIRClient class (lines 16-241)
Why: This is the public API. Everything a consumer does starts here: FHIRClient(settings=...) or FHIRClient(state=...). It orchestrates auth, patient management, and state persistence. Read __init__ (lines 31-87), state/from_state (lines 214-238), and the auth delegation methods (lines 126-162).
2. fhirclient/server.py — FHIRServer class (lines 34-314)
Why: This is the HTTP layer. It handles all REST calls, CapabilityStatement fetching, auth delegation, and error mapping. The get_capability() method (lines 71-102) is where the auth instance is created from the server's security statement — a critical flow.
3. fhirclient/auth.py — FHIRAuth + FHIROAuth2Auth (lines 13-420)
Why: The auth strategy hierarchy. The factory (create(), line 89), the OAuth2 flow (authorize_uri → handle_callback → _request_access_token), and PKCE implementation are all here. The _request_access_token method (lines 302-339) is where the token exchange happens.
4. demos/flask/flask_app.py — Reference Implementation (lines 1-134)
Why: This is the canonical example of how to use the library. It shows the full lifecycle: _get_smart() (state restoration), smart.handle_callback() (OAuth callback), smart.patient (patient access), and _save_state (persistence). Read this first to understand the intended usage pattern.
5. fhirclient/_utils.py — Pagination Utilities (lines 1-110)
Why: The iter_pages() generator (lines 95-110) is the pagination API consumers will use. The _sanitize_next_link() function (lines 48-73) has the docstring/code mismatch noted above.
Known Gotchas
1. State Restoration Cannot Clear Values
File: fhirclient/client.py:228-238, server.py:313, auth.py:142
The or operator in from_state means falsy values ("", 0, None, False) in the state dict are silently ignored. To clear a field, you must explicitly set it to a non-falsy sentinel or call the dedicated reset methods (e.g., reset_patient() at line 207).
2. _sanitize_next_link Does Not Validate Hostname
File: fhirclient/_utils.py:48-73
Despite the docstring claiming hostname validation against the origin server, only scheme and netloc presence are checked. This is a security theater issue — a malicious next link pointing to a different server would pass validation.
3. base_uri Trailing Slash is Enforced Silently
File: fhirclient/server.py:49-50
self.base_uri = base_uri if "/" == base_uri[-1] else base_uri + "/"
If you pass "https://example.com/fhir", it becomes "https://example.com/fhir/". But urljoin behavior depends on this trailing slash — without it, the last path component is replaced. This is correct but easy to miss when debugging URL construction.
4. FHIRServer Constructor Raises Exception if base_uri is Too Short
File: fhirclient/server.py:55-58
The magic number 10 means "http://a.bc" (12 chars) works but "http://a.b" (10 chars) fails. This is a heuristic that could break with very short but valid URIs (e.g., localhost testing with short hostnames).
5. Patient Read Retry is Exactly One Attempt
File: fhirclient/client.py:174-179
If the first Patient.read gets a 401 and reauthorize() succeeds, the retry happens. But if the retry also gets a 401, the exception propagates uncaught. There is no circuit breaker or exponential backoff.
6. expires_at Check Uses Local Clock
File: fhirclient/auth.py:168-171
@property
def ready(self):
if self.expires_at and self.expires_at < datetime.now():
self.reset()
return True if self.access_token else False
This compares against datetime.now() (local time), not UTC. If the server's expires_in is interpreted as UTC seconds but the local clock is in a different timezone, tokens could expire prematurely or persist too long. The expires_at is computed as datetime.now() + timedelta(seconds=expires_in) at line 325, so it's consistently local — but this is still a timezone sensitivity.
7. save_state() is Called on Every State Mutation
File: fhirclient/client.py:162, 183, 212, 240
Every significant operation (launch context handling, patient read, reset, explicit save) calls save_state(), which invokes the user-provided save_func. In the Flask demo, this writes to the session on every request. For high-throughput apps, this could be a performance concern — there's no batching or debouncing.