Dashboard

Troubleshooting

Common issues and solutions when working with the SHL SDK.

Installation & Setup

"Cannot find module '@fhirfly-io/shl'"

Ensure you've installed the package:

npm install @fhirfly-io/shl

The SDK requires Node.js 18+ for built-in crypto and fetch support.

"FHIRFLY_API_KEY is not defined"

FHIRfly API enrichment is optional. If you don't have an API key, skip the fhirfly parameter on clinical data methods:

// Without enrichment (manual code input)
bundle.addMedication({
  code: "00071015523",
  system: "http://hl7.org/fhir/sid/ndc",
  display: "Lisinopril 10mg Tablet",
});

// With enrichment (adds SNOMED mappings, display names automatically)
bundle.addMedication({ byNDC: "00071015523", fhirfly: client.ndc });

Sign up at fhirfly.io for a free API key if you want enrichment.

Bundle Building

"ValidationError: bundle is required and must be an object"

The SHL.create() function expects the result of bundle.build(), not the IPS.Bundle instance:

// Wrong
const result = await SHL.create({ bundle: myBundle, storage });

// Right
const fhirBundle = await myBundle.build();
const result = await SHL.create({ bundle: fhirBundle, storage });

"Invalid NDC code" or enrichment returns null

NDC codes must be the 11-digit format (with leading zeros). Check your code format:

// Wrong: 10-digit NDC
bundle.addMedication({ byNDC: "0071015523" });

// Right: 11-digit NDC (pad segment to 5-4-2)
bundle.addMedication({ byNDC: "00071015523" });

If enrichment returns null, the code may not exist in the FHIRfly database. Use debug: true to inspect the raw bundle and verify the resource was added.

FHIR Validator warnings (dom-6, ips-2)

These are common IPS profile warnings, not errors:

Warning Meaning Fix
dom-6 Missing narrative text.div The SDK generates narrative automatically — update to latest version
ips-2 Missing section.text in Composition Expected for machine-generated IPS; safe to ignore

Encryption & SHL Creation

"debug mode is not allowed when NODE_ENV=production"

Debug mode stores unencrypted PHI alongside the JWE. This is blocked in production to prevent accidental data exposure. Remove debug: true from your SHL.create() options.

"Failed to store content: ..."

Storage backend errors typically indicate:

  • LocalStorage: Directory doesn't exist or isn't writable. Ensure the directory path exists.
  • S3Storage: Missing AWS credentials or incorrect bucket/region. Check AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY.
  • FhirflyStorage: Invalid API key or quota exceeded. Check your plan limits at Pricing.

Server & Manifest

Server returns 500 on manifest POST

  1. Check that the SHL files exist in your storage backend ({shlId}/manifest.json, {shlId}/content.jwe, {shlId}/metadata.json)
  2. Verify your baseUrl matches the actual URL your server is running on
  3. Check server logs — the SDK handler logs errors via the onAccess callback

"Invalid passcode" (401) when passcode is correct

Passcodes are hashed with SHA-256 before storage. If you're comparing manually, ensure you're comparing hashes, not plaintext. The SDK handler does this automatically.

If you're hitting rate limits (429), wait for the retry_after_sec value in the response before retrying.

SHL returns 410 (Gone)

The SHL has either:

  • Expired: The expiresAt date has passed
  • Exhausted: The maxAccesses limit was reached

Create a new SHL if the content still needs to be shared. Expiration and access limits cannot be modified after creation.

Consumption & Decryption

"Failed to decrypt: ..."

Common causes:

  • Wrong key: The decryption key must match the one embedded in the shlink:/ URL. Use SHL.decode(url).key.
  • Corrupted JWE: The fetched content was modified in transit. Ensure your server returns the JWE as-is without transformation.
  • Not a JWE: You may be passing HTML or an error response instead of the JWE string. Check that the content endpoint returns application/jose.

Handling embedded vs. location manifest entries

Some SHL servers return inline JWE content instead of URLs. Use SHL.getEntryContent() to handle both:

const decoded = SHL.decode(shlUrl);
const manifest = await fetch(decoded.url, {
  method: "POST",
  body: JSON.stringify({ passcode }),
}).then(r => r.json());

for (const entry of manifest.files) {
  const jwe = await SHL.getEntryContent(entry);
  const { contentType, data } = SHL.decryptContent(jwe, decoded.key);
}

Patient Input Format

The SDK accepts two formats for patient demographics:

// Structured (FHIR-native, recommended)
new IPS.Bundle({
  given: "Maria",
  family: "Garcia",
  birthDate: "1985-03-15",
  gender: "female",
});

// Simplified (convenience)
new IPS.Bundle({
  name: "Maria Garcia",
  birthDate: "1985-03-15",
});

Both produce valid FHIR Patient resources. The constructor takes patient demographics directly (no patient: wrapper). Use the structured format for production to preserve given/family name separation.

Self-Hosted vs. FhirflyStorage

See Security & Compliance for a full comparison. Key differences:

Concern FhirflyStorage Self-Hosted (S3/Azure/GCS)
Access count atomicity Atomic (MongoDB $inc) Best-effort (read-modify-write)
Passcode hashing SHA-256 + timingSafeEqual SHA-256 + timingSafeEqual
Rate limiting 3-tier IP-based Your responsibility
Server-side encryption AES-256 (automatic) Depends on bucket config

For strict one-time access links (maxAccesses: 1), use FhirflyStorage or add your own atomic counter.

Still stuck?