instructure/canvas-lms · error
Unable to fetch export data from #
Error message
Unable to fetch export data from #{@status_url}: #{response.code} #{response.message}. (#{response.body}) What it means
LTI content migration via ContentMigrationService polls the tool's status URL for export results. retrieve_export fetches @fetch_url (derived from @status_url) via CanvasHttp and raises this error when the HTTP response is anything other than 200, embedding the status code, reason phrase, and response body for diagnosis. A Timeout::Error becomes a separate tool-specific timeout message.
Solutions
- Read the embedded response.body in the message — it usually states whether the export was not found, unauthorized, or a tool-side error
- Confirm @status_url/@fetch_url and base_request_headers (OAuth signing, keys) are correct for the current tool configuration
- Retry the migration: re-run start! to get a fresh export if the tool reports the job expired or not found
- Check the LTI tool's server logs for the corresponding request to identify 4xx vs 5xx root cause
- Add retry/backoff around CanvasHttp.get for transient 5xx/502/503 gateway errors
Example fix
// before
response = CanvasHttp.get(@fetch_url, base_request_headers)
raise "Unable to fetch export data from #{@status_url}: ..." unless response.code.to_i == 200
// after
response = CanvasHttp.get(@fetch_url, base_request_headers)
if response.code.to_i == 200
JSON.parse(response.body)
elsif %w[429 502 503 504].include?(response.code)
retry_later_with_backoff
else
raise "Unable to fetch export data from #{@status_url}: ..."
end Defensive patterns
Strategy: retry
Try / catch
begin
data = exporter.retrieve_export
rescue RuntimeError => e
raise unless e.message.start_with?('Unable to fetch export data')
if e.message =~ /\b(502|503|504|429)\b/
retry_with_backoff
else
mark_migration_failed(e.message)
end
end Prevention
- Validate the tool's status_url responds 200 during tool configuration/health checks
- Keep signed request headers (OAuth) in sync with the tool's current keys
- Set an export TTL retry so stale exports are re-requested via start!
- Monitor tool endpoints for 5xx rates before launching migrations
When it happens
Trigger: The external LTI tool returns 4xx/5xx on the export status endpoint: expired/invalid signed request, export not found (job expired or already deleted), tool-side crash (500), or a proxy/gateway error between Canvas and the tool.
Common situations: Tool's export expired because the migration sat in queue too long; OAuth/signature mismatch after rotating tool credentials; tool deployed behind a CDN that returns 403/504; misconfigured @status_url pointing at the wrong host.
Understand the failure class
Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.
Related errors
- could not retrieve configuration, the server response timed…
- Couldn't send LTI AGS grade progress metric
- Fetching data from #
- Unable to start import for external tool #
- Content-Type must be 'application/xml'
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/706e6806641ad88b.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/lti/content_migration_service/exporter.rb:73
false
end
end
rescue Timeout::Error
false
end
def retrieve_export
return nil if @export_status == FAILED_STATUS
response = Canvas.retriable(on: Timeout::Error) do
InstrumentTLSCiphers.without_tls_metrics do
CanvasHttp.get(@fetch_url, base_request_headers)
end
end
if response.code.to_i == 200
JSON.parse(response.body)
else
raise "Unable to fetch export data from #{@status_url}: #{response.code} #{response.message}. (#{response.body})"
end
rescue Timeout::Error
raise "Fetching data from #{@tool.name} timed out."
end
def start!
return if defined? @status_url
InstrumentTLSCiphers.without_tls_metrics do
response = Canvas.retriable(on: Timeout::Error) do
case export_format
when JSON_FORMAT
CanvasHttp.post(export_start_url, base_request_headers, body: start_export_post_body.to_json, content_type: "application/json")
else
CanvasHttp.post(export_start_url, base_request_headers, form_data: Rack::Utils.build_nested_query(start_export_post_body))
end
end
case response.code.to_iView on GitHub (pinned to 1c9f0bb801)