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
- Use exactly one of SMTPEndpoint, HTTPEndpoint, AddressEndpoint as the class prefix
- Build the reference from the model rather than by hand: "#{endpoint.class.name}##{endpoint.uuid}"
- Read the allowed set programmatically instead of hard-coding: Route::ENDPOINT_TYPES
- 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
- Never hand-write 'Class#uuid' strings; derive them: "#{endpoint.class.name}##{endpoint.uuid}"
- Reference Route::ENDPOINT_TYPES instead of duplicating the list in client code
- Add a form/API whitelist so unknown type names fail validation, not with a 500
- After upgrades, re-check that stored type names still appear in ENDPOINT_TYPES
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
- Invalid endpoint class name '#{class_name}'
- Invalid email address
- OIDC cannot be used unless enabled in the configuration
- No host was given for the request
- Could not resolve '#{@host}' to any IP address
AI-assisted analysis of postalserver/postal@d038eaa8c7 (2026-08-21).
Data as JSON: /api/errors/b795d1995cb09a72.
Report an issue: GitHub.