concise_errors 0.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 746b9cdaa164daed16b8bc70adba8cb822ef52c9039a441ab1fc29d92c53e48f
4
- data.tar.gz: 6af78b52ef0ce3cae853b72a379e9c744b55c06d6324acfd66d776d7870dc6c1
3
+ metadata.gz: 27769238dc3e569de3a89c3c9ff4f3fcbd43d858f7c77533df92571b66abce74
4
+ data.tar.gz: f4bb5b6d61033967083e9ddcbc069d05acabbf155c8ce806e1dfb32076731a97
5
5
  SHA512:
6
- metadata.gz: 4695c72ba6c43566bc957f975fb1c190f1bcbd66572d847554f8ea725fec6686be2990e8862c9b040f7367f3a169a1699a501b083965885c173ac69574410471
7
- data.tar.gz: 4097761cb0f2a54d2d2f3c40eab9870c842c00f7a4efbbb82c4d31baa5316077e8160656ce891e96d76f9cd4134343a9cab61b33d69fb3f40320b049897b70be
6
+ metadata.gz: bc5f4bbff9b8ec08847430d9d5dfd5feec8eccbf29a6d8a3fa77f598d9674bc2f36ed8a0426a056aac703218710c1dea1a1a2eaeff319fc84b1e4491abaef767
7
+ data.tar.gz: 70e5c1412e25f8e379a9e2058eb1bcddeb65169783d3d3661adb9dce1ee8432ebe2909884bc59216984ff8da6f809ac8a5439be9430374fcd1b6a6418a7598b4
data/.rubocop.yml CHANGED
@@ -8,3 +8,11 @@ Style/StringLiterals:
8
8
 
9
9
  Style/StringLiteralsInInterpolation:
10
10
  EnforcedStyle: double_quotes
11
+
12
+ Metrics/ClassLength:
13
+ Exclude:
14
+ - "lib/concise_errors/formatter.rb"
15
+
16
+ Metrics/MethodLength:
17
+ Exclude:
18
+ - "lib/concise_errors/formatter.rb"
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.2.0] - 2025-10-25
4
+
5
+ - Add a "View full Rails error page" button to the HTML view.
6
+ - Clicking the button replays the request (method, headers, and body when applicable)
7
+ to the same URL with the `concise_errors_full=1` flag, letting Rails render its
8
+ original full-featured debug page (or Web Console when present).
9
+ - Configurable via `config.concise_errors.full_error_param` (defaults to
10
+ `"concise_errors_full"`). Set to `nil` or empty to disable the button entirely.
11
+
3
12
  ## [0.1.0] - 2025-10-22
4
13
 
5
14
  - Initial release
data/README.md CHANGED
@@ -8,6 +8,7 @@ ConciseErrors swaps Rails’ `ActionDispatch::DebugExceptions` middleware with a
8
8
  - Optional single-file HTML view optimised for dark/light mode without external CSS.
9
9
  - Configurable backtrace depth with an omission indicator.
10
10
  - Automatic fallback to `text/plain` when the request is `xhr?` or clients negotiate non-HTML `Accept` headers.
11
+ - HTML view includes a button to open Rails' full-featured error page.
11
12
 
12
13
  ## Installation
13
14
 
@@ -21,7 +22,7 @@ Run `bundle install` and restart your Rails server. ConciseErrors automatically
21
22
 
22
23
  ## Configuration
23
24
 
24
- ConciseErrors ships with opinionated defaults — HTML output, no CSS, and Web Console middleware is automaticallyremoved in development — so simply installing the gem is enough. Override anything you need from `config/application.rb` or an environment-specific config:
25
+ ConciseErrors ships with opinionated defaults — HTML output, no CSS, and Web Console middleware is automatically removed in development — so simply installing the gem is enough. Override anything you need from `config/application.rb` or an environment-specific config:
25
26
 
26
27
  ```ruby
27
28
  # config/environments/development.rb
@@ -32,6 +33,7 @@ Rails.application.configure do
32
33
  cfg.enabled = true # flip to false to restore the stock debug page
33
34
  cfg.application_root = Rails.root.to_s # optional: trim this prefix from traces
34
35
  cfg.logger = Rails.logger # optional: reuse your preferred logger
36
+ cfg.full_error_param = "concise_errors_full" # optional: query param to trigger full page; set nil/"" to disable
35
37
  end
36
38
  end
37
39
  ```
@@ -40,6 +42,13 @@ You can also steer the default format via `ENV["CONCISE_ERRORS_FORMAT"]` (`text`
40
42
 
41
43
  ConciseErrors only affects the debug middleware (the screen you see when `config.consider_all_requests_local` is true). Production 500 pages continue to use whatever `ActionDispatch::ShowExceptions` is configured to serve.
42
44
 
45
+ ### Viewing the full Rails error page
46
+
47
+ When rendering HTML, ConciseErrors shows a "View full Rails error page" button. Clicking it performs a same-origin request back to the same URL with the `concise_errors_full=1` flag (or your configured `full_error_param`). The original request method, CSRF token, and content type are preserved; for non-GET/HEAD requests the request body is replayed.
48
+
49
+ - To disable the button entirely, set `config.concise_errors.full_error_param = nil` or `""`.
50
+ - If Web Console is installed, the fallback will go through its middleware; otherwise Rails' stock `ActionDispatch::DebugExceptions` page is shown.
51
+
43
52
  ## Sample Output
44
53
 
45
54
  Plain text format (default):
@@ -6,7 +6,7 @@ module ConciseErrors
6
6
  DEFAULT_STACK_LINES = 10
7
7
  DEFAULT_FORMAT = :text
8
8
 
9
- attr_accessor :stack_trace_lines, :enabled, :logger, :application_root, :cleaner
9
+ attr_accessor :stack_trace_lines, :enabled, :logger, :application_root, :cleaner, :full_error_param
10
10
 
11
11
  def initialize
12
12
  reset!
@@ -31,6 +31,7 @@ module ConciseErrors
31
31
  @logger = nil
32
32
  @application_root = nil
33
33
  @cleaner = nil
34
+ @full_error_param = "concise_errors_full"
34
35
  end
35
36
  end
36
37
  end
@@ -10,6 +10,12 @@ module ConciseErrors
10
10
  # Replacement middleware that keeps the existing API while producing concise output.
11
11
  class DebugExceptions < ::ActionDispatch::DebugExceptions
12
12
  def call(env)
13
+ request = ActionDispatch::Request.new(env)
14
+
15
+ return fallback_web_console.call(env) if web_console_request?(request)
16
+
17
+ return fallback_web_console.call(env) if full_error_requested?(request)
18
+
13
19
  env["action_dispatch.backtrace_cleaner"] ||= ConciseErrors.configuration.cleaner
14
20
  super
15
21
  end
@@ -24,5 +30,31 @@ module ConciseErrors
24
30
  def logger(request)
25
31
  ConciseErrors.logger || super
26
32
  end
33
+
34
+ def full_error_requested?(request)
35
+ flag = ConciseErrors.configuration.full_error_param
36
+ return false if flag.nil? || flag.to_s.empty?
37
+
38
+ request.query_parameters.key?(flag.to_s)
39
+ end
40
+
41
+ def web_console_request?(request)
42
+ return false unless defined?(WebConsole::Middleware)
43
+
44
+ mount_point = WebConsole::Middleware.mount_point
45
+ return false if mount_point.nil? || mount_point.empty?
46
+
47
+ request.path.start_with?(mount_point)
48
+ end
49
+
50
+ def fallback_debug_exceptions
51
+ @fallback_debug_exceptions ||= ::ActionDispatch::DebugExceptions.new(@app)
52
+ end
53
+
54
+ def fallback_web_console
55
+ return fallback_debug_exceptions unless defined?(WebConsole::Middleware)
56
+
57
+ @fallback_web_console ||= WebConsole::Middleware.new(fallback_debug_exceptions)
58
+ end
27
59
  end
28
60
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "erb"
4
+ require "json"
4
5
  require "rack/utils"
5
6
 
6
7
  module ConciseErrors
@@ -48,6 +49,7 @@ module ConciseErrors
48
49
  <h1>#{html_escape(heading_line)}</h1>
49
50
  <p>#{html_escape(status_line)}</p>
50
51
  <pre>#{html_escape(truncated_trace.join("\n"))}</pre>
52
+ #{full_error_controls}
51
53
  </body>
52
54
  </html>
53
55
  HTML
@@ -91,5 +93,125 @@ module ConciseErrors
91
93
 
92
94
  line.sub(%r{\A#{Regexp.escape(root)}/?}, "./")
93
95
  end
96
+
97
+ def full_error_controls
98
+ return "" unless flag_parameter
99
+
100
+ payload = full_error_payload
101
+ return "" unless payload
102
+
103
+ button_id = "concise-errors-view-full"
104
+ payload_id = "#{button_id}-payload"
105
+ payload_json = ERB::Util.json_escape(JSON.generate(payload))
106
+
107
+ <<~HTML
108
+ <button type="button" id="#{button_id}">View full Rails error page</button>
109
+ <script type="application/json" id="#{payload_id}">#{payload_json}</script>
110
+ <script>
111
+ (function() {
112
+ var button = document.getElementById("#{button_id}");
113
+ var payloadScript = document.getElementById("#{payload_id}");
114
+ if (!button || !payloadScript) { return; }
115
+
116
+ if (!window.fetch) {
117
+ button.addEventListener("click", function() {
118
+ window.location.assign("#{html_escape(payload.fetch(:url))}");
119
+ });
120
+ return;
121
+ }
122
+
123
+ var payloadText = payloadScript.textContent || payloadScript.innerText || "{}";
124
+ var data;
125
+
126
+ try {
127
+ data = JSON.parse(payloadText);
128
+ } catch (error) {
129
+ console.error("Failed to parse full error request payload", error);
130
+ return;
131
+ }
132
+
133
+ button.addEventListener("click", function(event) {
134
+ event.preventDefault();
135
+
136
+ var options = {
137
+ method: data.method,
138
+ credentials: "same-origin"
139
+ };
140
+
141
+ if (data.headers && Object.keys(data.headers).length > 0) {
142
+ options.headers = data.headers;
143
+ }
144
+
145
+ if (data.body && data.body.length > 0 && data.method !== "GET" && data.method !== "HEAD") {
146
+ options.body = data.body;
147
+ }
148
+
149
+ fetch(data.url, options).then(function(response) {
150
+ return response.text().then(function(html) {
151
+ var doc = document;
152
+ doc.open();
153
+ doc.write(html);
154
+ doc.close();
155
+ });
156
+ }).catch(function(error) {
157
+ console.error("Failed to load full Rails error page", error);
158
+ });
159
+ });
160
+ })();
161
+ </script>
162
+ HTML
163
+ end
164
+
165
+ def full_error_payload
166
+ return unless flagged_url
167
+
168
+ {
169
+ method: request.request_method,
170
+ url: flagged_url,
171
+ body: replay_body,
172
+ headers: replay_headers
173
+ }
174
+ end
175
+
176
+ def replay_body
177
+ return "" if %w[GET HEAD].include?(request.request_method)
178
+
179
+ request.raw_post.to_s
180
+ rescue EOFError
181
+ ""
182
+ end
183
+
184
+ def replay_headers
185
+ headers = {}
186
+ content_type = request.get_header("CONTENT_TYPE").to_s
187
+ headers["Content-Type"] = content_type unless content_type.empty?
188
+
189
+ csrf = request.get_header("HTTP_X_CSRF_TOKEN").to_s
190
+ headers["X-CSRF-Token"] = csrf unless csrf.empty?
191
+
192
+ headers
193
+ end
194
+
195
+ def flagged_url
196
+ flag = flag_parameter
197
+ return unless flag
198
+
199
+ params = request.query_parameters.dup
200
+ params[flag] = "1"
201
+
202
+ query = Rack::Utils.build_nested_query(params)
203
+ base_path = request.fullpath.split("?", 2).first
204
+ return base_path if query.empty?
205
+
206
+ "#{base_path}?#{query}"
207
+ end
208
+
209
+ def flag_parameter
210
+ value = configuration.full_error_param
211
+ return if value.nil?
212
+
213
+ string = value.to_s.strip
214
+ string.empty? ? nil : string
215
+ end
94
216
  end
95
217
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ConciseErrors
4
- VERSION = "0.1.0"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -9,12 +9,14 @@ module ConciseErrors
9
9
  @logger: untyped
10
10
  @application_root: String?
11
11
  @cleaner: untyped
12
+ @full_error_param: String?
12
13
 
13
14
  attr_accessor stack_trace_lines: Integer?
14
15
  attr_accessor enabled: bool
15
16
  attr_accessor logger: untyped
16
17
  attr_accessor application_root: String?
17
18
  attr_accessor cleaner: untyped
19
+ attr_accessor full_error_param: String?
18
20
 
19
21
  def initialize: () -> void
20
22
  def format: () -> Symbol
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: concise_errors
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Obie Fernandez
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-10-23 00:00:00.000000000 Z
11
+ date: 2025-10-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actionpack