Skip to content

Compare the base API, not to_s, when refreshing a mounted app#2788

Open
ericproulx wants to merge 1 commit into
masterfrom
robust_mounted_app_comparison
Open

Compare the base API, not to_s, when refreshing a mounted app#2788
ericproulx wants to merge 1 commit into
masterfrom
robust_mounted_app_comparison

Conversation

@ericproulx

Copy link
Copy Markdown
Contributor

Problem

refresh_already_mounted deduplicates endpoints for the "same" mounted app with a string compare:

endpoints.delete_if { |endpoint| endpoint.mounted_app.to_s == app.to_s }

This only works by accident. mount turns every mounted Grape API into a throwaway mount_instance, and mount_instance builds a fresh Class.new(@base_parent) on every call (lib/grape/api.rb), so two mounts of the same API are distinct objectsapp == mounted_app is always false. What makes the .to_s compare land is that Grape::API::Instance delegates to_s to its @base (the original API):

def_delegators :@base, :to_s   # lib/grape/api/instance.rb

So mounted_app.to_s == app.to_s is really asking "do these two throwaway instances share the same base API?" — through a string proxy. That's brittle: it's an identity check dressed up as a string compare, and two distinct APIs that happen to share a name (same to_s) falsely dedup.

Fix

Compare the base API by identity — the real signal the string proxy stands in for. @base was writer-only (base=), so this exposes a reader and compares it:

def same_mounted_app?(mounted, app)
  return mounted.base.equal?(app.base) if mounted.respond_to?(:base) && app.respond_to?(:base)

  mounted.equal?(app)
end

Plain Rack apps have no base and are mounted as-is (same object across mounts), so they fall back to object identity.

Tests

  • Existing refresh_already_mounted specs still pass (same API → same base → deduped; no flag → duplicated).
  • Added a spec that forces a #to_s collision between two distinct APIs and asserts the refresh keys off base identity, not the string — it fails on the old to_s compare (removes the wrong endpoint) and passes with this change.
  • Full suite: 2352 examples, 0 failures; RuboCop clean.

🤖 Generated with Claude Code

`refresh_already_mounted` deduplicates endpoints for the "same" mounted
app by comparing `endpoint.mounted_app.to_s == app.to_s`. That only works
by accident: `mount` turns every mounted Grape API into a throwaway
`mount_instance` (a fresh `Class.new(@base_parent)` per mount), so two
mounts of the same API are distinct objects, and `Grape::API::Instance`
delegates `to_s` to its `@base` (the original API). The comparison is
really "same base API", done through a string proxy -- brittle (two APIs
that happen to share a name collide) and unclear.

Expose the base API via a reader and compare it by identity instead. Plain
Rack apps have no base and are mounted as-is, so they fall back to object
identity.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@ericproulx ericproulx force-pushed the robust_mounted_app_comparison branch from 90344ff to 0aee2c2 Compare July 8, 2026 07:14
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Danger Report

No issues found.

View run

@ericproulx ericproulx requested a review from dblock July 8, 2026 07:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant