postalserver/postal · error · Postal::Error

Invalid endpoint class name '#{class_name}'

Error message

Invalid endpoint class name '#{class_name}'

What it means

Route's virtual attribute _endpoint= accepts strings of the form 'ClassName#uuid' (what the admin UI and API submit). It splits on '#' and requires the class part to be one of Route::ENDPOINT_TYPES (SMTPEndpoint, HTTPEndpoint, AddressEndpoint); anything else raises Postal::Error before constantize is attempted, preventing arbitrary class instantiation from user input.

Source

Thrown at app/models/route.rb:80

    end
  end

  def _endpoint
    if mode == "Endpoint"
      @endpoint ||= endpoint ? "#{endpoint.class}##{endpoint.uuid}" : nil
    else
      @endpoint ||= mode
    end
  end

  def _endpoint=(value)
    if value.blank?
      self.endpoint = nil
      self.mode = nil
    elsif value =~ /\#/
      class_name, id = value.split("#", 2)
      unless ENDPOINT_TYPES.include?(class_name)
        raise Postal::Error, "Invalid endpoint class name '#{class_name}'"
      end

      self.endpoint = class_name.constantize.find_by_uuid(id)
      self.mode = "Endpoint"
    else
      self.endpoint = nil
      self.mode = value
    end
  end

  def forward_address
    @forward_address ||= "#{token}@#{Postal::Config.dns.route_domain}"
  end

  def wildcard?
    name == "*"
  end

View on GitHub (pinned to d038eaa8c7)

Solutions

  1. Use exactly one of SMTPEndpoint, HTTPEndpoint, AddressEndpoint as the class prefix
  2. Build the reference from the model rather than by hand: "#{endpoint.class.name}##{endpoint.uuid}"
  3. Read the allowed set programmatically instead of hard-coding: Route::ENDPOINT_TYPES
  4. Validate the string client-side/admin-side before assignment (see typeGuard)

Example fix

# before
route._endpoint = "HttpEndpoint##{http_endpoint.uuid}"  # wrong casing -> Postal::Error

# after
route._endpoint = "HTTPEndpoint##{http_endpoint.uuid}"
# or derive it: http_endpoint._endpoint / "#{endpoint.class.name}##{endpoint.uuid}"
Defensive patterns

Strategy: type-guard

Validate before calling

# before assigning route._endpoint
class_name, id = value.split("#", 2)
raise ArgumentError, "Unknown endpoint type #{class_name}" unless Route::ENDPOINT_TYPES.include?(class_name) && id.present?

Type guard

def valid_endpoint_reference?(value)
  return false unless value.is_a?(String) && value.include?("#")
  class_name, id = value.split("#", 2)
  Route::ENDPOINT_TYPES.include?(class_name) && id.present?
end

Try / catch

begin
  route._endpoint = value
rescue Postal::Error => e
  # client-supplied type name: reject with 422 echoing the allowed list
  render json: { error: e.message, allowed: Route::ENDPOINT_TYPES }, status: :unprocessable_entity
end

Prevention

When it happens

Trigger: Assigning route.endpoint_attributes/_endpoint a value like 'HttpEndpoint#...' (JS-style casing), 'CredentialEndpoint#...', 'SMTPServer#...', or a plain string containing a '#' with a made-up prefix; typically via the routes form/API when creating or updating a route for a server.

Common situations: API clients guessing or hard-coding type names with wrong casing; client code written against older/renamed endpoint class names; hand-built payload strings where the uuid separator or class name is malformed; admin UI tampering.

Related errors


AI-assisted analysis of postalserver/postal@d038eaa8c7 (2026-08-21). Data as JSON: /api/errors/b795d1995cb09a72. Report an issue: GitHub.