Repository navigation
Content Extractors Limits and Overrides
The TMI server extracts text from operator-trusted documents (DOCX, PPTX, XLSX, PDF, HTML, plain text) so Timmy and the rest of the content pipeline can index it. The OOXML extractors (DOCX, PPTX, XLSX) ship with a layered set of limits: operator-tunable defaults, hardcoded server ceilings, and per-user concurrency overrides. This page is the operator's reference for those controls and the failure-classification codes they produce.
The
content_extractors.*limits on this page are operational settings — DB-backed, seeded from defaults on first run, and editable at runtime via the admin API. See Configuration-Model for how operational config is stored and edited, Managing-Operational-Settings for the admin-API workflow, and Configuration-Reference for the fullcontent_extractors.*key list.
The content pipeline is invoked when a threat model imports or refreshes a document via a configured content source (Google Drive, OneDrive, Confluence, etc.). Each fetched byte stream goes through:
- Source fetch — bounded by the SafeHTTPClient's body cap (operator-configured, separate from this page).
- Extractor dispatch — based on declared content type, the pipeline routes to the matching extractor: plain text, HTML, PDF, DOCX, PPTX, XLSX.
- Per-user concurrency limiter — the user owning the document holds a semaphore slot for the duration of the extraction.
- Wall-clock budget — a single hard timeout caps how long any extraction may run.
- Output — Markdown (≤ the configured size cap) emitted to the caller.
The OOXML extractors enforce additional structural caps on top of the byte caps — slide count, cell count, ZIP nesting depth, XML element depth, ZIP compression ratio, and so on. Most are hardcoded ceilings, not operator-tunable; see the table below.
The values below are the project-wide defaults from internal/config/content_extractors.go::DefaultContentExtractorsConfig. These are what your server uses if no content_extractors: block exists in YAML and no TMI_CONTENT_EXTRACTORS_* environment variable is set.
| Knob | Default | Hardcoded ceiling |
|---|---|---|
compressed_size_bytes |
20 MiB | 50 MiB |
decompressed_size_bytes |
50 MiB | 100 MiB |
part_size_bytes |
20 MiB | 50 MiB |
pptx_slides |
100 | 250 |
xlsx_cells |
1000 | 10000 |
markdown_size_bytes |
128 KiB | 256 KiB |
wall_clock_budget |
30 s | 60 s |
per_user_concurrency_default |
2 | 16 |
If the YAML or environment value exceeds the ceiling, the server refuses to start with a clear validation error. Defaults below the ceiling are deliberately conservative — bump them when you have evidence (real users hitting them, or trusted machine accounts with known-large documents).
These are not operator-tunable. They are constants in internal/config/content_extractors.go and api/content_extractor_ooxml_common.go. They exist to bound worst-case resource usage even if an operator misconfigures the tunable knobs.
| Constant | Purpose |
|---|---|
maxCompressedSizeBytes (50 MiB) |
Hard ceiling on the on-disk OOXML container size. |
maxDecompressedSizeBytes (100 MiB) |
Hard ceiling on the sum of all decompressed parts inside the OOXML zip. Limits zip-bomb damage. |
maxPartSizeBytes (50 MiB) |
Hard ceiling on any single part inside the zip. |
maxPPTXSlides (250) |
Hard ceiling on slide count for PPTX. |
maxXLSXCells (10 000) |
Hard ceiling on populated cell count across all sheets for XLSX. |
maxMarkdownSizeBytes (256 KiB) |
Hard ceiling on the extractor's emitted Markdown. |
maxWallClockBudget (60 s) |
Hard ceiling on the per-extraction wall-clock timeout. |
MaxPerUserConcurrency (16) |
Hard ceiling on per-user concurrent extractions, including override values. |
Additional structural limits live in the OOXML common module:
- ZIP nesting depth cap (rejects nested-archive bombs).
- ZIP path traversal guard (rejects entries with
..segments or absolute paths). - ZIP compression-ratio cap (rejects entries with absurd decompression ratios).
- XML element-depth cap (rejects billion-laughs / quadratic-blowup XML).
content_extractors:
compressed_size_bytes: 20971520 # 20 MiB
decompressed_size_bytes: 52428800 # 50 MiB
part_size_bytes: 20971520 # 20 MiB
pptx_slides: 100
xlsx_cells: 1000
markdown_size_bytes: 131072 # 128 KiB
wall_clock_budget: 30s # any time.ParseDuration string
per_user_concurrency_default: 2Any field omitted falls back to its default.
Each YAML key has a matching env var. Env vars override YAML.
| YAML key | Environment variable |
|---|---|
compressed_size_bytes |
TMI_CONTENT_EXTRACTORS_COMPRESSED_SIZE_BYTES |
decompressed_size_bytes |
TMI_CONTENT_EXTRACTORS_DECOMPRESSED_SIZE_BYTES |
part_size_bytes |
TMI_CONTENT_EXTRACTORS_PART_SIZE_BYTES |
pptx_slides |
TMI_CONTENT_EXTRACTORS_PPTX_SLIDES |
xlsx_cells |
TMI_CONTENT_EXTRACTORS_XLSX_CELLS |
markdown_size_bytes |
TMI_CONTENT_EXTRACTORS_MARKDOWN_SIZE_BYTES |
wall_clock_budget |
TMI_CONTENT_EXTRACTORS_WALL_CLOCK_BUDGET |
per_user_concurrency_default |
TMI_CONTENT_EXTRACTORS_PER_USER_CONCURRENCY_DEFAULT |
Boot-time validation (ContentExtractorsConfig.Validate) rejects any value that's ≤ 0 or above its hardcoded ceiling.
Most users get the operator-configured default (per_user_concurrency_default, default 2). A trusted machine account that legitimately drives many concurrent extractions — for example, an automation that bulk-imports a document corpus — can be granted a higher allowance via a per-user override stored on the users table.
Only for trusted machine principals (service accounts, addons, internal automations) where you've measured the load and know the server can absorb it. Don't raise individual human users' overrides for convenience; bump the global default if many users are hitting it.
UPDATE users
SET extraction_concurrency_override = 8
WHERE internal_uuid = '<uuid>';The lookup is a SELECT extraction_concurrency_override FROM users WHERE internal_uuid = ? cached for the lifetime of the user's first extraction in the process. Override changes don't resize an existing semaphore — restart the server (or wait for the user's worker to drop out of cache) to apply the new value. This is a known limitation of the limiter in cmd/server/main.go::buildContentPipeline.
The override is hard-clamped to MaxPerUserConcurrency (16). A value above 16 in the database is silently treated as 16. A NULL override means "use the operator default."
UPDATE users
SET extraction_concurrency_override = NULL
WHERE internal_uuid = '<uuid>';SELECT internal_uuid, email, extraction_concurrency_override
FROM users
WHERE extraction_concurrency_override IS NOT NULL;When extraction fails, the document row's access_status is set to extraction_failed and access_reason_code records the specific failure. tmi-ux localizes user-facing messages by this stable code; operators read it to triage.
These constants live in api/access_diagnostics.go:
| Reason code | What happened |
|---|---|
extraction_limit:compressed_size |
The on-disk OOXML container exceeded compressed_size_bytes. Bump that knob if the value is legitimate. |
extraction_limit:decompressed_size |
Sum of decompressed parts exceeded decompressed_size_bytes. Either a legitimately large document or a zip bomb — inspect first. |
extraction_limit:part_size |
A single part inside the zip exceeded part_size_bytes. |
extraction_limit:part_count |
The OOXML container has more parts than the structural cap allows. |
extraction_limit:markdown_size |
The extractor's emitted Markdown exceeded markdown_size_bytes. Bump that knob if the document genuinely has that much text. |
extraction_limit:timeout |
Extraction ran past wall_clock_budget. Bump the budget or investigate why the document is so slow to parse (often a malformed XML structure). |
extraction_limit:xml_depth |
XML element depth exceeded the structural cap. Almost always a malformed document or an attempted XML bomb. |
extraction_limit:zip_nested |
OOXML zip contains another zip past the nesting cap. Almost always a zip bomb. |
extraction_limit:zip_path |
A zip entry has a .. segment or an absolute path. Reject — never bump. |
extraction_limit:compression_ratio |
A zip entry's decompression ratio is implausibly high. Reject — never bump. |
extraction_malformed |
The extractor parsed the file but the structure was invalid (e.g., an XLSX missing its workbook part). |
extraction_unsupported |
The pipeline routed the document to an extractor that doesn't support its declared subtype. |
extraction_internal |
An unexpected error inside the extractor. Investigate via server logs. |
Most extraction_limit:* codes are operator-tunable: bump the knob if the limit is legitimate. The path-traversal, nested-zip, and compression-ratio codes are never tunable — they exist to reject hostile input.
Likely causes, in order of likelihood:
- The document source upstream is unreachable — check
access_statusfirst. If it's notextraction_failed, the failure is upstream of the extractor (token revoked, source down, etc.). See Delegated Content Providers. - The server is under load and the per-user limiter is queueing extractions past the wall-clock budget. Check server CPU and the
oncalllog forextraction queue depthwarnings. - An operator just changed a YAML knob below a value users were already exceeding. Roll back the knob.
SELECT
id, name, content_type, access_status, access_reason_code, access_status_updated_at
FROM documents
WHERE access_status = 'extraction_failed'
AND threat_model_id = '<tm-uuid>'
ORDER BY access_status_updated_at DESC
LIMIT 25;The access_reason_code column tells you which limit (if any) was hit. Cross-reference with the table above.
For deeper debugging, consult /logs (see the operator Cloud Logging page) — the extractor logs the reason-code tag plus any underlying error message at WARN or ERROR level.
Restart the server and check the startup banner. Look for the OOXML extractors enabled log line:
OOXML extractors enabled: docx, pptx, xlsx (per-user concurrency default: 2, wall-clock budget: 30s)
Both numbers should match what you put in YAML or env vars. If they don't, the server fell back to defaults — re-check the env-var name (capitalization is significant).
-
internal/config/content_extractors.go— defaults, ceilings, validator. -
api/access_diagnostics.go— reason-code constants. -
api/content_pipeline.go— pipeline wiring. -
cmd/server/main.go::buildContentPipeline— runtime assembly and the per-user override lookup. - Originating issue: #287 (OOXML extractors feature).
- Using TMI for Threat Modeling
- Accessing TMI
- Authentication
- Identity Linking
- Creating Your First Threat Model
- Understanding the User Interface
- Working with Data Flow Diagrams
- Managing Threats
- Collaborative Threat Modeling
- Using Notes and Documentation
- Timmy AI Assistant
- Metadata and Extensions
- Planning Your Deployment
- Terraform Deployment (AWS)
- Deploying TMI Server
- Oracle Container Images
- Deploying TMI Web Application
- Setting Up Authentication
- Database Setup
- Bootstrapping Production
- Component Integration
- Post-Deployment
- Branding and Customization
- Monitoring and Health
- Cloud Logging
- Configuring Local Development
- Managing Operational Settings
- Content Extractors - Limits and Overrides
- Database Operations
- Database Security Strategies
- Transaction Isolation
- Oracle Content Feedback FK Cleanup
- Security Operations
- Secret Rotation
- Performance and Scaling
- Maintenance Tasks
- Getting Started with Development
- Local Development Cluster
- Architecture and Design
- API Integration
- Testing
- Contributing
- Extending TMI
- Dependency Upgrade Plans
- DFD Graphing Library Reference
- Migration Instructions
- Issue Tracker Integration
- Webhook Integration
- Addon System
- MCP Integration
- Delegated Content Providers
- Setting Up Google Content Providers
- API Clients
- API Client Maintenance
- Database Tool Reference
- TMI Terraform Analyzer
- WebSocket Test Harness