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

  1. Verify PV4 authentication configuration (secret/keys in Canvas and PV4 service) match and were rotated on both sides together.
  2. Check server clock sync (NTP) if the auth scheme includes timestamps.
  3. Confirm the environment has the required PV4 credentials set at all (missing config renders invalid headers).
  4. 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

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

Related errors


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)