Skip to content

Commit

Permalink
Add raise_on_js_error option
Browse files Browse the repository at this point in the history
  • Loading branch information
abrom committed Oct 10, 2024
1 parent 38e4b2d commit 5f6682e
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 38 deletions.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,9 @@ The `wait_for_timeout` option can also be used to wait the specified number of m
The `raise_on_request_failure` option, when enabled, will raise a `Grover::JavaScript::RequestFailedError`
if the initial content request or any subsequent asset request returns a bad response or times out.

The `raise_on_js_error` option, when enabled, will raise a `Grover::JavaScript::PageRenderError` if any uncaught
Javascript errors occur when trying to render the page.

The Chrome/Chromium executable path can be overridden with the `executable_path` option.

Supplementary JavaScript can be executed on the page (after render and before conversion to PDF/image)
Expand Down
12 changes: 12 additions & 0 deletions lib/grover/errors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ class Grover
module JavaScript # rubocop:disable Style/Documentation
Error = Class.new(::Grover::Error)
UnknownError = Class.new(Error)

ErrorWithDetails = Class.new(Error) do
def initialize(name, error_details)
super(name)
@error_details = Grover::Utils.deep_transform_keys_in_object error_details, &:to_sym
end

attr_reader :error_details
end
RequestFailedError = Class.new(ErrorWithDetails)
PageRenderError = Class.new(ErrorWithDetails)

def self.const_missing(name)
const_set name, Class.new(Error)
end
Expand Down
69 changes: 49 additions & 20 deletions lib/grover/js/processor.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,32 @@ const fs = require('fs');
const os = require('os');
const path = require('path');

function GroverError(name, errors) {
this.name = name;
this.message = errors.map(e => e.message).join("\n");
this.errors = errors;
}
GroverError.prototype = Error.prototype;

const _processPage = (async (convertAction, uriOrHtml, options) => {
let browser, page, errors = [], tmpDir, wsConnection = false;
let browser, page, tmpDir, wsConnection = false;
const requestErrors = [], pageErrors = [];

const captureRequestError = (request) => {
const requestError = { url: request.url() };

if (request.failure()) {
requestError.reason = request.failure().errorText;
requestError.message = requestError.reason + " at " + requestError.url;
} else if (request.response() && request.response().status()) {
requestError.status = request.response().status();
requestError.message = requestError.status + " " + requestError.url;
} else {
requestError.message = "UnknownError " + requestError.url;
}

requestErrors.push(requestError);
};

try {
// Configure puppeteer debugging options
Expand Down Expand Up @@ -163,12 +187,24 @@ const _processPage = (async (convertAction, uriOrHtml, options) => {
const raiseOnRequestFailure = options.raiseOnRequestFailure; delete options.raiseOnRequestFailure;
if (raiseOnRequestFailure) {
page.on('requestfinished', (request) => {
if (request.response() && !(request.response().ok() || request.response().status() === 304) && !request.redirectChain().length > 0) {
errors.push(request);
if (request.response() &&
!(request.response().ok() || request.response().status() === 304) &&
!request.redirectChain().length > 0) {
captureRequestError(request);
}
});
page.on('requestfailed', (request) => {
errors.push(request);
captureRequestError(request);
});
}

const raiseOnJSError = options.raiseOnJSError; delete options.raiseOnJSError;
if (raiseOnJSError) {
page.on('pageerror', (error) => {
pageErrors.push({
message: error.toString().replace(new RegExp('^' + error.name + ': '), ''),
type: error.name || 'Error'
});
});
}

Expand Down Expand Up @@ -257,21 +293,12 @@ const _processPage = (async (convertAction, uriOrHtml, options) => {
await page.hover(hoverSelector);
}

if (errors.length > 0) {
function RequestFailedError(errors) {
this.name = "RequestFailedError";
this.message = errors.map(e => {
if (e.failure()) {
return e.failure().errorText + " at " + e.url();
} else if (e.response() && e.response().status()) {
return e.response().status() + " " + e.url();
} else {
return "UnknownError " + e.url()
}
}).join("\n");
}
RequestFailedError.prototype = Error.prototype;
throw new RequestFailedError(errors);
if (requestErrors.length > 0) {
throw new GroverError("RequestFailedError", requestErrors);
}

if (pageErrors.length > 0) {
throw new GroverError("PageRenderError", pageErrors);
}

// Setup conversion timeout
Expand Down Expand Up @@ -301,7 +328,9 @@ const _processPage = (async (convertAction, uriOrHtml, options) => {
});

function _handleError(error) {
if (error instanceof Error) {
if (error instanceof GroverError) {
process.stdout.write(JSON.stringify(['err', error.message, error.name, error.errors]));
} else if (error instanceof Error) {
process.stdout.write(
JSON.stringify(['err', error.toString().replace(new RegExp('^' + error.name + ': '), ''), error.name])
);
Expand Down
3 changes: 2 additions & 1 deletion lib/grover/options_fixer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def fix_options!(*option_paths)
def fix_boolean_options!
fix_options!(
'display_header_footer', 'full_page', 'landscape', 'omit_background', 'prefer_css_page_size',
'print_background', 'viewport.has_touch', 'viewport.is_landscape', 'viewport.is_mobile', 'bypass_csp'
'print_background', 'viewport.has_touch', 'viewport.is_landscape', 'viewport.is_mobile', 'bypass_csp',
'raise_on_request_failure', 'raise_on_js_error'
) { |value| !FALSE_VALUES.include?(value) }
end

Expand Down
4 changes: 2 additions & 2 deletions lib/grover/processor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,14 +83,14 @@ def call_js_method(method, url_or_html, options) # rubocop:disable Metrics/AbcSi
input = stdout.gets
raise Errno::EPIPE, "Can't read from worker" if input.nil?

status, message, error_class = JSON.parse(input)
status, message, error_class, errors = JSON.parse(input)

if status == 'ok'
message
elsif error_class.nil?
raise Grover::JavaScript::UnknownError, message
else
raise Grover::JavaScript.const_get(error_class, false), message
raise Grover::JavaScript.const_get(error_class, false).new(*[message, errors].compact)
end
rescue JSON::ParserError
raise Grover::Error, 'Malformed worker response'
Expand Down
3 changes: 2 additions & 1 deletion lib/grover/utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ class Utils
ACRONYMS = {
'css' => 'CSS',
'csp' => 'CSP',
'http' => 'HTTP'
'http' => 'HTTP',
'js' => 'JS'
}.freeze
private_constant :ACRONYMS

Expand Down
72 changes: 72 additions & 0 deletions spec/grover/configuration_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,76 @@
it { is_expected.to eq 'https://my.domain' }
end
end

describe '#ignore_path' do
subject(:ignore_path) { configuration.ignore_path }

it { is_expected.to be_nil }

context 'when configured differently' do
before { configuration.ignore_path = '/foo/bar' }

it { is_expected.to eq '/foo/bar' }
end
end

describe '#use_pdf_middleware' do
subject(:use_pdf_middleware) { configuration.use_pdf_middleware }

it { is_expected.to be true }

context 'when configured differently' do
before { configuration.use_pdf_middleware = false }

it { is_expected.to be false }
end
end

describe '#use_png_middleware' do
subject(:use_png_middleware) { configuration.use_png_middleware }

it { is_expected.to be false }

context 'when configured differently' do
before { configuration.use_png_middleware = true }

it { is_expected.to be true }
end
end

describe '#use_jpeg_middleware' do
subject(:use_jpeg_middleware) { configuration.use_jpeg_middleware }

it { is_expected.to be false }

context 'when configured differently' do
before { configuration.use_jpeg_middleware = true }

it { is_expected.to be true }
end
end

describe '#node_env_vars' do
subject(:node_env_vars) { configuration.node_env_vars }

it { is_expected.to eq({}) }

context 'when configured differently' do
before { configuration.node_env_vars = { 'LD_PRELOAD' => '' } }

it { is_expected.to eq({ 'LD_PRELOAD' => '' }) }
end
end

describe '#allow_file_uris' do
subject(:allow_file_uris) { configuration.allow_file_uris }

it { is_expected.to be false }

context 'when configured differently' do
before { configuration.allow_file_uris = true }

it { is_expected.to be true }
end
end
end
78 changes: 68 additions & 10 deletions spec/grover/processor_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -697,10 +697,15 @@
end

it do
expect do
convert
end.to raise_error Grover::JavaScript::RequestFailedError,
"net::ERR_NAME_NOT_RESOLVED at #{protocol}://foo.bar/baz.img"
expect { convert }.to raise_error do |error|
expect(error).to be_a Grover::JavaScript::RequestFailedError
expect(error.message).to eq "net::ERR_NAME_NOT_RESOLVED at #{protocol}://foo.bar/baz.img"
expect(error.error_details).to eq [{
url: "#{protocol}://foo.bar/baz.img",
reason: 'net::ERR_NAME_NOT_RESOLVED',
message: "net::ERR_NAME_NOT_RESOLVED at #{protocol}://foo.bar/baz.img"
}]
end
end
end

Expand All @@ -715,18 +720,28 @@
</html>
HTML
end
let(:error_message) do
let(:error_details) do
if puppeteer_version_on_or_after? '22.6.0'
'net::ERR_BLOCKED_BY_ORB at https://google.com/404.jpg'
{
url: 'https://google.com/404.jpg',
reason: 'net::ERR_BLOCKED_BY_ORB',
message: 'net::ERR_BLOCKED_BY_ORB at https://google.com/404.jpg'
}
else
'404 https://google.com/404.jpg'
{
url: 'https://google.com/404.jpg',
status: 404,
message: '404 https://google.com/404.jpg'
}
end
end

it do
expect do
convert
end.to raise_error Grover::JavaScript::RequestFailedError, error_message
expect { convert }.to raise_error do |error|
expect(error).to be_a Grover::JavaScript::RequestFailedError
expect(error.message).to eq error_details[:message]
expect(error.error_details).to eq [error_details]
end
end
end

Expand Down Expand Up @@ -769,6 +784,49 @@
end
end

context 'when raise on JS error option is specified' do
let(:options) { basic_header_footer_options.merge('raiseOnJSError' => raise_errors) }
let(:raise_errors) { true }

context 'when a failure occurs it raises an error' do
let(:url_or_html) do
<<-HTML
<html>
<head><link rel="icon" href="data:;base64,iVBORw0KGgo="></head>
<body>
Success?
<script>Something went wrong</script>
<script>throw "Really wrong"</script>
</body>
</html>
HTML
end

it do
expect { convert }.to raise_error do |error|
expect(error).to be_a Grover::JavaScript::PageRenderError
expect(error.message).to eq "Unexpected identifier 'went'\nReally wrong"
expect(error.error_details).to eq [
{
type: 'SyntaxError',
message: "Unexpected identifier 'went'"
},
{
type: 'Error',
message: 'Really wrong'
}
]
end
end

context 'when `raiseOnJSError` is disabled' do
let(:raise_errors) { false }

it { expect(pdf_text_content).to eq "#{date} Success? #{protocol}://www.example.net/foo/bar 1/1" }
end
end
end

context 'when waitForSelector option is specified with options' do
let(:url_or_html) do
<<-HTML
Expand Down
25 changes: 21 additions & 4 deletions spec/grover/utils_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -298,12 +298,24 @@
it { is_expected.to eq('fooBar' => 'baz') }
end

context 'when key has an acronym in it' do
let(:object) { { prefer_css_page_size: true, bypass_csp: false, extra_http_headers: { 'Foo' => 'Bar' } } }
context 'when keys have acronyms in them' do
let(:object) do
{
prefer_css_page_size: true,
bypass_csp: false,
extra_http_headers: { 'Foo' => 'Bar' },
raise_on_js_error: true
}
end

it 'returns the acronym components of the keys uppercase' do
expect(normalize_object).to(
eq('preferCSSPageSize' => true, 'bypassCSP' => false, 'extraHTTPHeaders' => { 'foo' => 'Bar' })
eq(
'preferCSSPageSize' => true,
'bypassCSP' => false,
'extraHTTPHeaders' => { 'foo' => 'Bar' },
'raiseOnJSError' => true
)
)
end

Expand All @@ -312,7 +324,12 @@

it 'returns the acronym components of the keys uppercase (but does not transform the extraHTTPHeaders value' do
expect(normalize_object).to(
eq('preferCSSPageSize' => true, 'bypassCSP' => false, 'extraHTTPHeaders' => { 'Foo' => 'Bar' })
eq(
'preferCSSPageSize' => true,
'bypassCSP' => false,
'extraHTTPHeaders' => { 'Foo' => 'Bar' },
'raiseOnJSError' => true
)
)
end
end
Expand Down

0 comments on commit 5f6682e

Please sign in to comment.