instructure/canvas-lms · error · Pv4Unauthorized
unauthorized request
Error message
unauthorized request
What it means
Pv4Client#fetch raises Pv4Unauthorized with message "unauthorized request" when the PV4 page-views API returns HTTP 401. The request's credentials (PV4 auth headers/JWT) were rejected or missing, so the PV4 service refused authentication.
Solutions
- Verify PV4 authentication configuration (secret/keys in Canvas and PV4 service) match and were rotated on both sides together.
- Check server clock sync (NTP) if the auth scheme includes timestamps.
- Confirm the environment has the required PV4 credentials set at all (missing config renders invalid headers).
- Rescue Pv4Unauthorized and fall back to an alternate page-view source (e.g. legacy page_views table) while fixing credentials.
Example fix
// before
begin
Pv4Client.for_user(user, filters)
rescue Pv4NotFound; end
// after
begin
Pv4Client.for_user(user, filters)
rescue Pv4Unauthorized
Rails.logger.error('PV4 auth rejected - check pv4 credentials/clock skew')
fallback_to_legacy_page_views(user)
end Defensive patterns
Strategy: fallback
Validate before calling
raise 'PV4 not configured' if Canvas::Plugin.find('pv4')&.settings&.values.any?(&:blank?) Try / catch
begin Pv4Client.for_user(user, filters) rescue Pv4Unauthorized fallback_to_legacy_page_views(user) end
Prevention
- Keep PV4 secrets in sync between Canvas and the PV4 service across rotations
- Run NTP on hosts so signed timestamps stay valid
- Add a health check that authenticates against PV4 and alerts on 401s
When it happens
Trigger: Calling Pv4Client.for_user when request_headers contain an expired, mis-signed, or missing PV4 auth token — e.g. PV4 encryption/secret keys rotated or differing between Canvas and the PV4 service, or clocks drifting so the signed token falls outside the accepted window.
Common situations: PV4 auth key mismatch after a config/secret rotation on one side only; missing PV4 secrets in an environment (stage/prod parity issues); clock skew between app and PV4 servers invalidating timestamps in the signed request.
Understand the failure class
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Related errors
- invalid request
- Content Export for Outcomes Service failed
- Developer key mismatch
- Error queueing export for Outcomes Service: #
- Error retrieving export for Outcomes Service: #
AI-assisted analysis of instructure/canvas-lms@1c9f0bb801 (2026-09-15).
Data as JSON: /api/errors/f4f507451e40f58d.
Report an issue: GitHub.
Appendix: source
Thrown at app/models/page_view/pv4_client.rb:60
limit: nil)
end_time ||= Time.now.utc
start_time ||= Time.at(0).utc
params = "start_time=#{start_time.utc.iso8601(PRECISION)}"
params << "&end_time=#{end_time.utc.iso8601(PRECISION)}"
params << "&#{cached_root_account_uuids_for(user:)}"
params << "&last_page_view_id=#{last_page_view_id}" if last_page_view_id
params << "&limit=#{limit}" if limit
response = CanvasHttp.get(
@uri.merge("users/#{user.global_id}/page_views?#{params}").to_s,
request_headers
)
case response.code.to_i
when 400
raise Pv4BadRequest, "invalid request"
when 401
raise Pv4Unauthorized, "unauthorized request"
when 404
raise Pv4NotFound, "resource not found"
when 429
raise Pv4TooManyRequests, "rate limit exceeded"
end
json =
begin
response.body.empty? ? {} : JSON.parse(response.body)
rescue JSON::ParserError
{}
end
raise Pv4EmptyResponse, "the response is empty or does not contain expected keys" unless json["page_views"]
json["page_views"].map! do |pv|
pv["session_id"] = pv.delete("sessionid")
vhost = pv.delete("vhost")
http_request = pv.delete("http_request")View on GitHub (pinned to 1c9f0bb801)