instructure/canvas-lms · warning · ActiveRecord::RecordNotFound
Import is still being processed
Error message
Import is still being processed
What it means
In the outcome import created_group_ids endpoint, Canvas raises ActiveRecord::RecordNotFound with 'Import is still being processed' when the import has neither failed nor succeeded (workflow_state is still processing/created). The rescue maps it to a 404 JSON response; the group IDs are not yet available.
Solutions
- Poll GET .../outcome_imports/:id until workflow_state == 'succeeded', then call created_group_ids
- Add a delay/backoff between import creation and the first results fetch
- Handle the 404 with this message by retrying after an interval rather than treating it as a permanent failure
- For 'latest', ensure no other import was started concurrently that is still processing
Example fix
# before
ids = api.get("/api/v1/accounts/1/outcome_imports/latest/created_group_ids")
# after
loop do
imp = api.get("/api/v1/accounts/1/outcome_imports/latest")
break if imp['workflow_state'] == 'succeeded'
sleep 5
end
ids = api.get("/api/v1/accounts/1/outcome_imports/latest/created_group_ids") Defensive patterns
Strategy: retry
Validate before calling
const state = (await api.get(`.../outcome_imports/${id}`)).workflow_state;
if (state !== 'succeeded') return null; // not ready yet, caller should wait Try / catch
async function fetchGroupIdsWithRetry(id, { tries = 12, delayMs = 10000 } = {}) {
for (let i = 0; i < tries; i++) {
try { return await fetchGroupIds(id); }
catch (e) { const msg = e.response?.data?.message; if (e.response?.status === 404 && msg === 'Import is still being processed') { await sleep(delayMs); continue; } throw e; }
}
throw new Error('import still processing after retries');
} Prevention
- Poll the import status endpoint with backoff until succeeded
- Never assume an import completes synchronously after POST
- Set a max retry budget and alert if exceeded
- Account for queue latency on large CSVs
When it happens
Trigger: GET .../outcome_imports/:id/created_group_ids (or :id = 'latest') immediately after POSTing a large outcome import, before the background importer has finished.
Common situations: Scripts that create an import and immediately fetch results without polling, large CSVs in queues under load, or checking 'latest' while a newer import is still running.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
- polling.poll_sessions.errors.course_required
- and cannot be used together
- A course with that id does not exist
- Anonymous assignments cannot be hidden by section
- Anonymous assignments cannot be posted by section
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/d2021b6685764c06.
Report an issue: GitHub.
Appendix: source
Thrown at app/controllers/outcome_imports_api_controller.rb:234
#
# Examples:
# curl 'https://<canvas>/api/v1/accounts/<account_id>/outcome_imports/outcomes_group_ids/<outcome_import_id>' \
# -H "Authorization: Bearer <token>"
# curl 'https://<canvas>/api/v1/courses/<course_id>/outcome_imports/outcome_group_ids/<outcome_import_id>' \
# -H "Authorization: Bearer <token>"
#
# @returns array of outcome ids
def created_group_ids
if authorized_action(@context, @current_user, %i[import_outcomes manage_outcomes])
begin
import = if params[:id] == "latest"
@context.latest_outcome_import or raise ActiveRecord::RecordNotFound
else
@context.outcome_imports.find(params[:id])
end
raise ActiveRecord::RecordNotFound, "Import has failed" if import.failed?
raise ActiveRecord::RecordNotFound, "Import is still being processed" unless import.succeeded?
render json: LearningOutcomeGroup.where(outcome_import_id: import.id).pluck(:id).map(&:to_s)
rescue ActiveRecord::RecordNotFound => e
render json: { message: e.message }, status: :not_found
end
end
end
private
def body_file
file_obj = request.body
# rubocop:disable Style/TrivialAccessors -- not a Class
file_obj.instance_exec do
def set_file_attributes(filename, content_type)
@original_filename = filename
@content_type = content_typeView on GitHub (pinned to 1c9f0bb801)