route_500_check 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6b15f117febf23b2ce99b03f0a0177aeca8aa24e1c4d1c71d0730b1bdaca5391
4
+ data.tar.gz: 941a95a6427a6f92b759d481be967ea01de4d6df909817c5ad5c9282e29dd397
5
+ SHA512:
6
+ metadata.gz: 86b6c5cd56e13f8c360a1d63b5d6ef80e59ccf659b34029b33840ec1593ee72379d81c12384a2de2845fa1bed94f734a07eec3ab393f1d12e7853197a419c190
7
+ data.tar.gz: 9aa0293e93bde3be504edd57f4eb87df565f9910fe2fb5592245b30494b49bbbcb34fb2f486e82b97e12e535c288fb4cf83ebdf77509f8b217493e9574d05556
data/CHANGELOG.md ADDED
@@ -0,0 +1,68 @@
1
+ # Changelog
2
+
3
+ ## v0.2.0
4
+
5
+ ### Summary
6
+
7
+ This release focuses on **stability, safety, and operational clarity**.
8
+ No new features were added. Existing functionality was refined to ensure
9
+ predictable behavior in production cron jobs and CI environments.
10
+
11
+ ---
12
+
13
+ ### Added
14
+
15
+ * Explicit **exit code semantics** to distinguish:
16
+
17
+ * HTTP 500 detection
18
+ * Configuration / safety stops
19
+ * Runtime failures
20
+ * `SECURITY.md` documenting intended usage, safety guarantees, and non-goals
21
+ * Validation layer (`DSL::Validator`) executed **before any HTTP requests**
22
+
23
+ ---
24
+
25
+ ### Changed
26
+
27
+ * Entry point unified through CLI (`exe/route500check` → `Route500Check.run`)
28
+ * DSL evaluation stabilized using instance-scoped execution
29
+ * Route expansion logic centralized in `RouteParamBuilder`
30
+ * Safety limit violations now raise a dedicated configuration exception
31
+ * Human-readable configuration errors clearly separated from runtime failures
32
+
33
+ ---
34
+
35
+ ### Safety Improvements
36
+
37
+ * Route expansion is fully deterministic and validated prior to execution
38
+ * No HTTP requests are sent when configuration validation fails
39
+ * Global and per-route limits are enforced consistently
40
+
41
+ ---
42
+
43
+ ### Exit Codes
44
+
45
+ | Code | Meaning |
46
+ | ---- | -------------------------------------------------------------- |
47
+ | 0 | No HTTP 500 errors detected |
48
+ | 1 | Configuration or safety limit triggered (no requests executed) |
49
+ | 2 | Runtime error or HTTP 500 detected |
50
+
51
+ ---
52
+
53
+ ### Compatibility Notes
54
+
55
+ * No changes to DSL syntax
56
+ * Existing configurations continue to work as-is
57
+ * Behavior is stricter where misconfiguration previously led to silent over-execution
58
+
59
+ ---
60
+
61
+ ### Notes
62
+
63
+ This version intentionally avoids feature expansion.
64
+ Future releases will prioritize correctness and maintainability over additional DSL features.
65
+
66
+ ---
67
+
68
+ © mntkst
data/LICENSE ADDED
@@ -0,0 +1,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 mnstkst
data/README.md ADDED
@@ -0,0 +1,255 @@
1
+ # Route500Check
2
+
3
+ Route500Check is a lightweight checker focused on **HTTP 500 errors** in Rails applications.
4
+ It does not depend on Rails internals and checks pages via real HTTP requests, just like end users.
5
+
6
+ * Focused on HTTP 500 detection
7
+ * Safe for large-scale sites (anti-runaway design)
8
+ * Suitable for CI / cron jobs
9
+
10
+ Route500Check is designed as a **safety-focused operational tool**, not a crawler or scanner.
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```ruby
17
+ gem "route_500_check"
18
+ ```
19
+
20
+ ```bash
21
+ bundle install
22
+ bundle exec rails generate route500_check:install
23
+ ```
24
+
25
+ ---
26
+
27
+ ## Basic Usage
28
+
29
+ ### config/route_500_check.rb
30
+
31
+ ```ruby
32
+ base_url "http://localhost:3000"
33
+ default_limit 100
34
+
35
+ route "/"
36
+
37
+ route "/foo/:id" do
38
+ id 1000..9999
39
+ sample 10
40
+ end
41
+ ```
42
+
43
+ ### Run
44
+
45
+ ```bash
46
+ bundle exec route500check
47
+ ```
48
+
49
+ ### Production (recommended)
50
+
51
+ ```bash
52
+ ROUTE500CHECK_BASE_URL=https://example.com ONLY=public bundle exec route500check
53
+ ```
54
+
55
+ ---
56
+
57
+ ## base_url Priority
58
+
59
+ ```
60
+ ENV["ROUTE500CHECK_BASE_URL"]
61
+
62
+ DSL base_url
63
+
64
+ error
65
+ ```
66
+
67
+ In production environments, it is recommended to use environment variables
68
+ instead of modifying the DSL configuration.
69
+
70
+ ---
71
+
72
+ ## ONLY=public
73
+
74
+ ```bash
75
+ ONLY=public bundle exec route500check
76
+ ```
77
+
78
+ Runs checks only for public-facing pages.
79
+
80
+ Default excluded prefixes:
81
+
82
+ ```
83
+ /admin
84
+ /api
85
+ /internal
86
+ /rails
87
+ /assets
88
+ /health
89
+ ```
90
+
91
+ Custom prefixes:
92
+
93
+ ```ruby
94
+ only_public do
95
+ exclude_prefix "/admin"
96
+ exclude_prefix "/api"
97
+ end
98
+ ```
99
+
100
+ ---
101
+
102
+ ## Safety Guards
103
+
104
+ Route500Check includes multiple safety mechanisms to prevent accidental overload
105
+ or misuse.
106
+
107
+ ### default_limit (global hard limit)
108
+
109
+ ```ruby
110
+ default_limit 100
111
+ ```
112
+
113
+ Absolute upper bound for the total number of expanded routes.
114
+
115
+ If the expanded route count exceeds this limit, execution stops **before any HTTP
116
+ requests are sent**.
117
+
118
+ ### route limit
119
+
120
+ ```ruby
121
+ limit 20
122
+ ```
123
+
124
+ Effective rule:
125
+
126
+ ```
127
+ min(route_limit, default_limit)
128
+ ```
129
+
130
+ ---
131
+
132
+ ## sample
133
+
134
+ ```ruby
135
+ sample 10
136
+ ```
137
+
138
+ * Random selection per route
139
+ * No duplicate URLs within the same route
140
+ * Applied before global limits
141
+
142
+ ---
143
+
144
+ ## ignore_status
145
+
146
+ ```ruby
147
+ ignore_status [502, 503]
148
+ ```
149
+
150
+ Affects exit code determination only.
151
+ Statuses are still logged and written to JSON output.
152
+
153
+ ---
154
+
155
+ ## Output
156
+
157
+ ### STDOUT
158
+
159
+ Human-readable logs intended for operators.
160
+
161
+ ### JSON (always generated)
162
+
163
+ File: `route_500_check_result.json`
164
+ Location: current working directory
165
+
166
+ Includes:
167
+
168
+ * Summary (status counts, latency)
169
+ * Detailed per-route results
170
+
171
+ ---
172
+
173
+ ## Exit Codes
174
+
175
+ Route500Check separates exit codes by intent, making it safe for automation.
176
+
177
+ | Exit code | Meaning |
178
+ | --------- | -------------------------------------------------------------- |
179
+ | `0` | No HTTP 500 errors detected |
180
+ | `1` | Configuration or safety limit triggered (no requests executed) |
181
+ | `2` | Runtime error or HTTP 500 detected |
182
+
183
+ ### Exit code `1` (Configuration / Safety)
184
+
185
+ Exit code `1` indicates that Route500Check stopped **before sending any HTTP
186
+ requests**, for example:
187
+
188
+ * Expanded route count exceeds `default_limit`
189
+ * Route sampling or limit configuration prevents safe execution
190
+
191
+ Example output:
192
+
193
+ ```text
194
+ [route_500_check][CONFIG] Route expansion exceeded safety limit.
195
+ - expanded routes: 122
196
+ - default_limit: 10
197
+
198
+ This is NOT a runtime error.
199
+ Adjust default_limit or route sample/limit settings.
200
+ ```
201
+
202
+ This exit code does **not** indicate HTTP 500 errors.
203
+
204
+ ---
205
+
206
+ ## Security Considerations
207
+
208
+ Route500Check is intended for **site owners and operators** to monitor their own
209
+ applications.
210
+
211
+ ### Controlled Request Expansion
212
+
213
+ To prevent accidental overload or abuse:
214
+
215
+ * All routes must be explicitly defined
216
+ * Total request count is validated **before execution**
217
+ * Execution stops immediately if safety limits are exceeded
218
+ * No HTTP requests are sent when validation fails
219
+
220
+ ### Not a Scanning Tool
221
+
222
+ Route500Check intentionally avoids features common in scanning or crawling tools:
223
+
224
+ * No automatic discovery
225
+ * No recursive crawling
226
+ * No parallel request amplification
227
+ * No authentication or authorization bypass
228
+
229
+ ### Clear Failure Semantics
230
+
231
+ Configuration and safety stops are clearly distinguished from runtime failures:
232
+
233
+ * Configuration / safety violations return exit code `1`
234
+ * Runtime errors and HTTP 500 detection return exit code `2`
235
+
236
+ This separation reduces the risk of misinterpretation in CI, cron jobs, and
237
+ monitoring systems.
238
+
239
+ ### Intended Usage
240
+
241
+ Route500Check is suitable for:
242
+
243
+ * Verifying critical user-facing routes
244
+ * Detecting HTTP 500 regressions
245
+ * Safe execution in production cron jobs and CI pipelines
246
+
247
+ It is **not intended for penetration testing, stress testing, or content
248
+ discovery**.
249
+
250
+ ---
251
+
252
+ ## License
253
+
254
+ MIT
255
+ © mntkst
data/SECURITY.md ADDED
@@ -0,0 +1,119 @@
1
+ # Security Policy
2
+
3
+ ## Purpose
4
+
5
+ Route500Check is designed as a **safety-focused operational tool** for site owners and operators.
6
+ Its primary purpose is to detect HTTP 500 errors on explicitly defined routes using real HTTP requests,
7
+ while preventing accidental overload or misuse.
8
+
9
+ This document explains the security-related design principles and intended usage of Route500Check.
10
+
11
+ ---
12
+
13
+ ## Intended Usage
14
+
15
+ Route500Check is intended for:
16
+
17
+ * Monitoring critical user-facing routes
18
+ * Detecting HTTP 500 regressions in Rails applications
19
+ * Safe execution in production cron jobs and CI pipelines
20
+
21
+ It is **not intended** for:
22
+
23
+ * Penetration testing
24
+ * Stress testing or load testing
25
+ * Crawling or content discovery
26
+ * Scanning systems you do not own or operate
27
+
28
+ ---
29
+
30
+ ## Controlled Request Execution
31
+
32
+ Route500Check includes multiple safeguards to ensure controlled execution:
33
+
34
+ * All routes must be explicitly defined by the user
35
+ * No automatic discovery or recursive crawling is performed
36
+ * Route expansion is deterministic and predictable
37
+ * Total request count is validated **before execution**
38
+
39
+ If safety validation fails, execution stops immediately and **no HTTP requests are sent**.
40
+
41
+ ---
42
+
43
+ ## Safety Limits
44
+
45
+ ### Global Limit (`default_limit`)
46
+
47
+ The `default_limit` setting defines an absolute upper bound for the total number of HTTP requests
48
+ that can be generated from all routes.
49
+
50
+ If the expanded route count exceeds this limit:
51
+
52
+ * Route500Check stops before sending any requests
53
+ * Exit code `1` is returned
54
+ * A clear configuration-related message is displayed
55
+
56
+ This mechanism prevents accidental overload due to misconfiguration.
57
+
58
+ ### Per-route Limits
59
+
60
+ Each route may define its own:
61
+
62
+ * `limit`
63
+ * `sample`
64
+
65
+ Effective limits are calculated safely, and always respect the global `default_limit`.
66
+
67
+ ---
68
+
69
+ ## Exit Code Semantics
70
+
71
+ Route500Check separates exit codes by intent to reduce operational risk:
72
+
73
+ | Exit code | Meaning |
74
+ | --------- | -------------------------------------------------------------- |
75
+ | `0` | No HTTP 500 errors detected |
76
+ | `1` | Configuration or safety limit triggered (no requests executed) |
77
+ | `2` | Runtime error or HTTP 500 detected |
78
+
79
+ Exit code `1` indicates a **safety stop**, not a runtime failure and not an HTTP 500 detection.
80
+
81
+ ---
82
+
83
+ ## Non-goals and Explicit Limitations
84
+
85
+ To avoid misuse, Route500Check intentionally does **not** provide:
86
+
87
+ * Parallel or high-volume request execution
88
+ * Automatic endpoint discovery
89
+ * Authentication or authorization bypass mechanisms
90
+ * Rate limit evasion or throttling circumvention
91
+
92
+ Any use outside the intended scope may violate laws, regulations, or terms of service.
93
+
94
+ ---
95
+
96
+ ## Responsible Use
97
+
98
+ Users are responsible for ensuring that Route500Check is executed only against systems they own
99
+ or are authorized to operate.
100
+
101
+ The authors and maintainers of Route500Check assume no responsibility for misuse of this tool
102
+ outside its intended operational scope.
103
+
104
+ ---
105
+
106
+ ## Reporting Security Issues
107
+
108
+ If you believe you have found a security vulnerability in Route500Check itself (not in a target
109
+ application), please report it responsibly.
110
+
111
+ At this time, please open a GitHub issue with a clear description of the problem and steps to
112
+ reproduce it. Sensitive details should be minimized.
113
+
114
+ ---
115
+
116
+ ## Policy Updates
117
+
118
+ This security policy may be updated as the project evolves. Please review it periodically to
119
+ stay informed about Route500Check's security posture.
data/exe/route500check ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "bundler/setup"
5
+ require "route_500_check"
6
+
7
+ # 実行は必ず CLI 経由に集約
8
+ Route500Check.run
@@ -0,0 +1,11 @@
1
+ module Route500Check
2
+ module Generators
3
+ class InstallGenerator < Rails::Generators::Base
4
+ source_root File.expand_path("templates", __dir__)
5
+
6
+ def copy_config
7
+ copy_file "route_500_check.rb", "config/route_500_check.rb"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,57 @@
1
+ # Route500Check configuration file
2
+ #
3
+ # This file defines which routes should be checked for HTTP 500 errors.
4
+ # Routes are expanded and validated before execution to ensure safe operation.
5
+ #
6
+ # No HTTP requests are sent if validation fails.
7
+
8
+ # Base URL for requests.
9
+ # In production, it is recommended to use the environment variable
10
+ # ROUTE500CHECK_BASE_URL instead of hard-coding this value.
11
+ #
12
+ # Example:
13
+ # ROUTE500CHECK_BASE_URL=https://example.com bundle exec route500check
14
+ #
15
+ base_url "http://localhost:3000"
16
+
17
+ # Global safety limit.
18
+ # This is an absolute upper bound for the total number of HTTP requests
19
+ # after route expansion.
20
+ #
21
+ default_limit 50
22
+
23
+ # Run checks only for public-facing pages.
24
+ # Useful when executing against production environments.
25
+ #
26
+ # only_public do
27
+ # exclude_prefix "/admin"
28
+ # exclude_prefix "/api"
29
+ # end
30
+
31
+ # -----------------------
32
+ # Route definitions
33
+ # -----------------------
34
+
35
+ # Simple static route
36
+ route "/"
37
+
38
+ # Route with path parameters
39
+ route "/items/:id" do
40
+ id 1..100
41
+ sample 10
42
+ end
43
+
44
+ # Route with explicit limit
45
+ route "/categories/:category_id" do
46
+ category_id [1, 2, 3, 4, 5]
47
+ limit 5
48
+ end
49
+
50
+ # -----------------------
51
+ # Optional settings
52
+ # -----------------------
53
+
54
+ # Ignore specific HTTP status codes when determining exit code.
55
+ # These statuses are still logged and written to JSON output.
56
+ #
57
+ # ignore_status [502, 503]
@@ -0,0 +1,71 @@
1
+ # lib/route_500_check/checker.rb
2
+
3
+ require "net/http"
4
+ require "uri"
5
+
6
+ module Route500Check
7
+ class Checker
8
+ def initialize(base_url)
9
+ @base_url = base_url
10
+ end
11
+
12
+ def check(path)
13
+ uri = build_uri(path)
14
+ start = now
15
+
16
+ response = Net::HTTP.get_response(uri)
17
+ finish = now
18
+
19
+ build_success_result(
20
+ path: path,
21
+ uri: uri,
22
+ response: response,
23
+ start: start,
24
+ finish: finish
25
+ )
26
+ rescue => e
27
+ finish = now rescue nil
28
+
29
+ build_error_result(
30
+ path: path,
31
+ uri: uri,
32
+ error: e,
33
+ start: start,
34
+ finish: finish
35
+ )
36
+ end
37
+
38
+ private
39
+
40
+ def build_uri(path)
41
+ URI.join(@base_url, path)
42
+ end
43
+
44
+ def now
45
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
46
+ end
47
+
48
+ def elapsed_ms(start, finish)
49
+ return nil unless start && finish
50
+ ((finish - start) * 1000).round(1)
51
+ end
52
+
53
+ def build_success_result(path:, uri:, response:, start:, finish:)
54
+ {
55
+ path: path,
56
+ url: uri.to_s,
57
+ status: response.code.to_i,
58
+ elapsed_ms: elapsed_ms(start, finish)
59
+ }
60
+ end
61
+
62
+ def build_error_result(path:, uri:, error:, start:, finish:)
63
+ {
64
+ path: path,
65
+ url: uri.to_s,
66
+ error: error.message,
67
+ elapsed_ms: elapsed_ms(start, finish)
68
+ }
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,21 @@
1
+ # lib/route500_check/cli.rb
2
+
3
+ module Route500Check
4
+ class CLI
5
+ EXIT_OK = 0
6
+ EXIT_500 = 2
7
+
8
+ def run
9
+ config = DSL.load!
10
+ DSL::Validator.validate!(config)
11
+
12
+ Runner.run
13
+ rescue Route500Check::Errors::RouteLimitExceeded => e
14
+ warn "[route_500_check][CONFIG] #{e.message}"
15
+ exit 1
16
+ rescue => e
17
+ warn "[route_500_check][FATAL] #{e.class}: #{e.message}"
18
+ exit 2
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,10 @@
1
+ # dsl/route_definition.rb
2
+ Route = Struct.new(:path, :expanded_paths, :limit)
3
+
4
+ module Route500Check::DSL
5
+ class RouteDefinition
6
+ def self.load
7
+ # DSLを読み、placeholder展開済みの Route を返す
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,23 @@
1
+ # lib/route_500_check/dsl/validator.rb
2
+ module Route500Check
3
+ class DSL::Validator
4
+ def self.validate!(config)
5
+ expanded =
6
+ RouteParamBuilder.expand(
7
+ config.routes,
8
+ config.default_limit
9
+ )
10
+
11
+ total = expanded.size
12
+ limit = config.default_limit
13
+
14
+ return unless limit
15
+ return if total <= limit
16
+
17
+ raise Route500Check::Errors::RouteLimitExceeded.new(
18
+ actual: total,
19
+ limit: limit
20
+ )
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,95 @@
1
+ # lib/route_500_check/dsl.rb
2
+ require "route_500_check/route_param_builder"
3
+ require "route_500_check/only_public_builder"
4
+
5
+ module Route500Check
6
+ RouteDef = Struct.new(:template, :params)
7
+
8
+ class DSL
9
+ CONFIG_PATH = "config/route_500_check.rb"
10
+
11
+ class << self
12
+ attr_accessor :current
13
+ end
14
+
15
+ attr_reader :routes, :ignore_statuses
16
+
17
+ def initialize
18
+ @routes = []
19
+ @default_limit = nil
20
+ @ignore_statuses = []
21
+ @only_public_prefixes = nil
22
+ @base_url = nil
23
+ end
24
+
25
+ # ==========
26
+ # Load config
27
+ # ==========
28
+ def self.load!
29
+ unless File.exist?(CONFIG_PATH)
30
+ raise <<~MSG
31
+ [route_500_check] Config file not found: #{CONFIG_PATH}
32
+
33
+ Please run:
34
+ rails generate route500_check:install
35
+ MSG
36
+ end
37
+
38
+ self.current = new
39
+
40
+ begin
41
+ DSL.current.instance_eval(File.read(CONFIG_PATH))
42
+ rescue Exception => e
43
+ raise "[route_500_check] Failed to load config: #{e.message}"
44
+ end
45
+
46
+ current
47
+ end
48
+
49
+ # ==========
50
+ # Global settings
51
+ # ==========
52
+ def base_url(value = nil)
53
+ value ? (@base_url = value) : @base_url
54
+ end
55
+
56
+ def default_limit(value = nil)
57
+ value ? (@default_limit = value) : @default_limit
58
+ end
59
+
60
+ def ignore_status(values = nil)
61
+ if values
62
+ @ignore_statuses = Array(values).map(&:to_i)
63
+ else
64
+ @ignore_statuses
65
+ end
66
+ end
67
+
68
+ # ==========
69
+ # ONLY=public DSL
70
+ # ==========
71
+ def only_public(&block)
72
+ builder = ::Route500Check::OnlyPublicBuilder.new
73
+ builder.instance_eval(&block)
74
+ @only_public_prefixes = builder.prefixes
75
+ end
76
+
77
+ def only_public_prefixes
78
+ @only_public_prefixes
79
+ end
80
+
81
+ # ==========
82
+ # route DSL
83
+ # ==========
84
+ def route(path, &block)
85
+ params = {}
86
+
87
+ if block
88
+ builder = ::Route500Check::RouteParamBuilder.new(params)
89
+ builder.instance_eval(&block)
90
+ end
91
+
92
+ @routes << RouteDef.new(path, params)
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,26 @@
1
+ module Route500Check
2
+ module Errors
3
+ class RouteLimitExceeded < StandardError
4
+ attr_reader :actual, :limit
5
+
6
+ def initialize(actual:, limit:)
7
+ @actual = actual
8
+ @limit = limit
9
+ super(build_message)
10
+ end
11
+
12
+ private
13
+
14
+ def build_message
15
+ <<~MSG.strip
16
+ Route expansion exceeded safety limit.
17
+ - expanded routes: #{actual}
18
+ - default_limit: #{limit}
19
+
20
+ This is NOT a runtime error.
21
+ Adjust default_limit or route sample/limit settings.
22
+ MSG
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,12 @@
1
+ # executor/request_runner.rb
2
+ module Route500Check::Executor
3
+ class RequestRunner
4
+ def self.run(routes, config)
5
+ routes.flat_map do |route|
6
+ route.expanded_paths.map do |path|
7
+ run_one(path, config)
8
+ end
9
+ end
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,45 @@
1
+ # lib/route_500_check/only_public_builder.rb
2
+
3
+ module Route500Check
4
+ class OnlyPublicBuilder
5
+ DEFAULT_EXCLUDE_PREFIXES = %w[
6
+ /admin
7
+ /api
8
+ /internal
9
+ /rails
10
+ /assets
11
+ /health
12
+ ].freeze
13
+
14
+ # ======================
15
+ # DSL 用(既存仕様そのまま)
16
+ # ======================
17
+ def initialize
18
+ @prefixes = []
19
+ end
20
+
21
+ def exclude_prefix(prefix)
22
+ @prefixes << prefix
23
+ end
24
+
25
+ def prefixes
26
+ @prefixes
27
+ end
28
+
29
+ # ======================
30
+ # 実行用(Runner から利用)
31
+ # ======================
32
+ def self.filter(paths, prefixes = nil)
33
+ effective_prefixes =
34
+ if prefixes && !prefixes.empty?
35
+ prefixes
36
+ else
37
+ DEFAULT_EXCLUDE_PREFIXES
38
+ end
39
+
40
+ paths.reject do |path|
41
+ effective_prefixes.any? { |prefix| path.start_with?(prefix) }
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,9 @@
1
+ module Route500Check
2
+ class Railtie < Rails::Railtie
3
+ railtie_name :route_500_check
4
+
5
+ generators do
6
+ require_relative "../generators/route_500_check/install_generator"
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ # lib/route_500_check/reporter/json.rb
2
+
3
+ require "json"
4
+ require "time"
5
+
6
+ module Route500Check
7
+ module Reporter
8
+ class Json
9
+ DEFAULT_OUTPUT_PATH = "route500check.json"
10
+
11
+ def self.write(results:, summary:, output_path: nil)
12
+ payload = build_payload(results: results, summary: summary)
13
+ File.write(output_path || DEFAULT_OUTPUT_PATH, JSON.pretty_generate(payload))
14
+ end
15
+
16
+ private
17
+
18
+ def self.build_payload(results:, summary:)
19
+ {
20
+ generated_at: Time.now.utc.iso8601,
21
+ summary: summary,
22
+ results: results
23
+ }
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,5 @@
1
+ # result/result.rb
2
+ Result = Struct.new(
3
+ :path, :url, :status, :elapsed_ms, :error,
4
+ keyword_init: true
5
+ )
@@ -0,0 +1,10 @@
1
+ # result/summary.rb
2
+ class Summary
3
+ def initialize(results)
4
+ @results = results
5
+ end
6
+
7
+ def has_500?
8
+ @results.any? { |r| r.status.to_i >= 500 }
9
+ end
10
+ end
@@ -0,0 +1,108 @@
1
+ # lib/route_500_check/route_param_builder.rb
2
+
3
+ module Route500Check
4
+ class RouteParamBuilder
5
+ # ======================
6
+ # DSL 用
7
+ # ======================
8
+ def initialize(params)
9
+ @params = params
10
+ end
11
+
12
+ def sample(n)
13
+ @params[:__sample__] = n
14
+ end
15
+
16
+ def limit(n)
17
+ @params[:__limit__] = n
18
+ end
19
+
20
+ def method_missing(name, *args, &block)
21
+ @params[name.to_sym] =
22
+ if args.length <= 1
23
+ args.first
24
+ else
25
+ args
26
+ end
27
+ end
28
+
29
+ def respond_to_missing?(_name, _include_private = false)
30
+ true
31
+ end
32
+
33
+ # ======================
34
+ # 実行用(Runner が呼ぶ)
35
+ # ======================
36
+ def self.expand(route_defs, default_limit)
37
+ route_defs.flat_map do |route|
38
+ template = route.template
39
+ params = route.params || {}
40
+
41
+ placeholders =
42
+ template.scan(/:(\w+)/).flatten.map(&:to_sym)
43
+
44
+ # placeholder が無い場合
45
+ if placeholders.empty?
46
+ paths = [template]
47
+ else
48
+ values_map =
49
+ placeholders.map do |key|
50
+ raw = params[key]
51
+ values =
52
+ case raw
53
+ when Range
54
+ raw.to_a
55
+ when Array
56
+ raw.flat_map { |v| v.is_a?(Range) ? v.to_a : v }
57
+ else
58
+ [raw]
59
+ end
60
+
61
+ [key, values]
62
+ end.to_h
63
+
64
+ combinations = cartesian_product(values_map)
65
+
66
+ # sample
67
+ if params[:__sample__]
68
+ combinations = combinations.sample(params[:__sample__])
69
+ end
70
+
71
+ # limit(route → global の順で適用)
72
+ route_limit = params[:__limit__]
73
+ global_limit = default_limit
74
+
75
+ effective_limit =
76
+ if route_limit && global_limit
77
+ [route_limit, global_limit].min
78
+ else
79
+ route_limit || global_limit
80
+ end
81
+
82
+ if effective_limit
83
+ combinations = combinations.take(effective_limit)
84
+ end
85
+
86
+ paths =
87
+ combinations.map do |combo|
88
+ path = template.dup
89
+ combo.each { |k, v| path.sub!(":#{k}", v.to_s) }
90
+ path
91
+ end
92
+ end
93
+
94
+ paths
95
+ end
96
+ end
97
+
98
+ def self.cartesian_product(values_map)
99
+ keys = values_map.keys
100
+ values = values_map.values
101
+
102
+ values
103
+ .shift
104
+ .product(*values)
105
+ .map { |combo| keys.zip(combo).to_h }
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,130 @@
1
+ # lib/route_500_check/runner.rb
2
+
3
+ require "route_500_check/route_param_builder"
4
+ require "route_500_check/only_public_builder"
5
+
6
+ module Route500Check
7
+ class Runner
8
+ EXIT_OK = 0
9
+ EXIT_500 = 2
10
+
11
+ def self.run(_options = {})
12
+ config = DSL.load!
13
+
14
+ runtime_base_url =
15
+ ENV["ROUTE500CHECK_BASE_URL"] ||
16
+ config.base_url
17
+
18
+ unless runtime_base_url
19
+ abort "[route_500_check] base_url is required (ENV or DSL)"
20
+ end
21
+
22
+ puts "[route_500_check] base_url=#{runtime_base_url}"
23
+
24
+ checker = Checker.new(runtime_base_url)
25
+
26
+ # ---- route expansion ----
27
+ paths =
28
+ RouteParamBuilder.expand(
29
+ config.routes,
30
+ config.default_limit
31
+ )
32
+
33
+ # ---- ONLY=public ----
34
+ if ENV["ONLY"] == "public"
35
+ paths =
36
+ OnlyPublicBuilder.filter(
37
+ paths,
38
+ config.only_public_prefixes
39
+ )
40
+ end
41
+
42
+ # ---- execute ----
43
+ results = []
44
+
45
+ paths.each do |path|
46
+ result = checker.check(path)
47
+ results << result
48
+
49
+ if result[:error]
50
+ puts "[route_500_check] ERROR #{result[:url]} #{result[:error]}"
51
+ else
52
+ puts "[route_500_check] GET #{result[:url]} -> #{result[:status]} (#{result[:elapsed_ms]}ms)"
53
+ end
54
+ end
55
+
56
+ # ---- summary ----
57
+ summary = build_summary(results)
58
+ print_summary(summary)
59
+
60
+ ignored = config.ignore_status || []
61
+
62
+ has_500 = results.any? do |r|
63
+ s = r[:status]
64
+ s && s >= 500 && !ignored.include?(s)
65
+ end
66
+
67
+ Reporter::Json.write(
68
+ results: results,
69
+ summary: summary
70
+ )
71
+
72
+ exit(has_500 ? EXIT_500 : EXIT_OK)
73
+ end
74
+
75
+ # ==========
76
+ # summary helpers
77
+ # ==========
78
+ def self.print_summary(s)
79
+ puts "[route_500_check] Result summary:"
80
+ puts " total_routes: #{s[:total_routes]}"
81
+ puts " success: #{s[:success]}"
82
+ puts " errors: #{s[:errors]}"
83
+ puts " http_2xx: #{s[:http_2xx]}"
84
+ puts " http_3xx: #{s[:http_3xx]}"
85
+ puts " http_4xx: #{s[:http_4xx]}"
86
+ puts " http_5xx: #{s[:http_5xx]}"
87
+ puts " max_latency: #{s[:max_latency_ms] ? "#{s[:max_latency_ms]}ms" : '-'}"
88
+ puts " avg_latency: #{s[:avg_latency_ms] ? "#{s[:avg_latency_ms]}ms" : '-'}"
89
+ end
90
+
91
+ def self.build_summary(results)
92
+ total = results.size
93
+
94
+ error_count = results.count { |r| r[:error] }
95
+
96
+ statuses = results.map { |r| r[:status] }.compact
97
+
98
+ http_2xx = statuses.count { |s| s >= 200 && s < 300 }
99
+ http_3xx = statuses.count { |s| s >= 300 && s < 400 }
100
+ http_4xx = statuses.count { |s| s >= 400 && s < 500 }
101
+ http_5xx = statuses.count { |s| s >= 500 }
102
+
103
+ http_4xx_breakdown =
104
+ statuses
105
+ .select { |s| s >= 400 && s < 500 }
106
+ .tally
107
+ .transform_keys(&:to_s)
108
+
109
+ success = total - error_count - http_5xx
110
+
111
+ elapsed = results.map { |r| r[:elapsed_ms] }.compact
112
+
113
+ {
114
+ total_routes: total,
115
+ success: success,
116
+ errors: error_count,
117
+
118
+ http_2xx: http_2xx,
119
+ http_3xx: http_3xx,
120
+ http_4xx: http_4xx,
121
+ http_5xx: http_5xx,
122
+
123
+ http_4xx_breakdown: http_4xx_breakdown,
124
+
125
+ max_latency_ms: elapsed.max,
126
+ avg_latency_ms: elapsed.any? ? (elapsed.sum / elapsed.size.to_f).round(1) : nil
127
+ }
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,3 @@
1
+ module Route500Check
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,31 @@
1
+ # lib/route_500_check.rb
2
+
3
+ require "route_500_check/version"
4
+
5
+ # DSL 関連
6
+ require "route_500_check/route_param_builder"
7
+ require "route_500_check/only_public_builder"
8
+ require "route_500_check/dsl"
9
+ require "route_500_check/dsl/validator"
10
+ require "route_500_check/errors"
11
+
12
+ # 実行系
13
+ require "route_500_check/checker"
14
+ require "route_500_check/reporter/json"
15
+ require "route_500_check/runner"
16
+ require "route_500_check/cli"
17
+
18
+ module Route500Check
19
+ def self.define(&block)
20
+ DSL.current ||= DSL.new
21
+ DSL.current.instance_eval(&block)
22
+ end
23
+
24
+ def self.run
25
+ CLI.new.run
26
+ end
27
+ end
28
+
29
+ if defined?(Rails)
30
+ require "route_500_check/railtie"
31
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: route_500_check
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - mntkst
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-12-31 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '6.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '6.0'
27
+ description: DSL-based HTTP 500 error detector for Rails applications
28
+ email:
29
+ executables:
30
+ - route500check
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - CHANGELOG.md
35
+ - LICENSE
36
+ - README.md
37
+ - SECURITY.md
38
+ - exe/route500check
39
+ - lib/generators/route_500_check/install_generator.rb
40
+ - lib/generators/route_500_check/templates/route_500_check.rb
41
+ - lib/route_500_check.rb
42
+ - lib/route_500_check/checker.rb
43
+ - lib/route_500_check/cli.rb
44
+ - lib/route_500_check/dsl.rb
45
+ - lib/route_500_check/dsl/route_definition.rb
46
+ - lib/route_500_check/dsl/validator.rb
47
+ - lib/route_500_check/errors.rb
48
+ - lib/route_500_check/executor/request_runner.rb
49
+ - lib/route_500_check/only_public_builder.rb
50
+ - lib/route_500_check/railtie.rb
51
+ - lib/route_500_check/reporter/json.rb
52
+ - lib/route_500_check/result/result.rb
53
+ - lib/route_500_check/result/summary.rb
54
+ - lib/route_500_check/route_param_builder.rb
55
+ - lib/route_500_check/runner.rb
56
+ - lib/route_500_check/version.rb
57
+ homepage: https://github.com/mntkst/route_500_check
58
+ licenses:
59
+ - MIT
60
+ metadata:
61
+ homepage_uri: https://github.com/mntkst/route_500_check
62
+ source_code_uri: https://github.com/mntkst/route_500_check
63
+ changelog_uri: https://github.com/mntkst/route_500_check/releases
64
+ post_install_message:
65
+ rdoc_options: []
66
+ require_paths:
67
+ - lib
68
+ required_ruby_version: !ruby/object:Gem::Requirement
69
+ requirements:
70
+ - - ">="
71
+ - !ruby/object:Gem::Version
72
+ version: '0'
73
+ required_rubygems_version: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ requirements: []
79
+ rubygems_version: 3.4.19
80
+ signing_key:
81
+ specification_version: 4
82
+ summary: Detect HTTP 500 errors by scanning selected Rails routes
83
+ test_files: []