Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

url: show input in parse error message #11934

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 7 additions & 17 deletions lib/internal/url.js
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,6 @@ class TupleOrigin {

function onParseComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
throw new TypeError('Invalid URL');
var ctx = this[context];
ctx.flags = flags;
ctx.scheme = protocol;
Expand All @@ -118,19 +116,23 @@ function onParseComplete(flags, protocol, username, password,
initSearchParams(this[searchParams], query);
}

function onParseError(flags, input) {
const error = new TypeError('Invalid URL: ' + input);
error.input = input;
throw error;
}

// Reused by URL constructor and URL#href setter.
function parse(url, input, base) {
const base_context = base ? base[context] : undefined;
url[context] = new StorageObject();
binding.parse(input.trim(), -1,
base_context, undefined,
onParseComplete.bind(url));
onParseComplete.bind(url), onParseError);
}

function onParseProtocolComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const newIsSpecial = (flags & binding.URL_FLAGS_SPECIAL) !== 0;
const s = this[special];
const ctx = this[context];
Expand Down Expand Up @@ -159,8 +161,6 @@ function onParseProtocolComplete(flags, protocol, username, password,

function onParseHostComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const ctx = this[context];
if (host) {
ctx.host = host;
Expand All @@ -174,8 +174,6 @@ function onParseHostComplete(flags, protocol, username, password,

function onParseHostnameComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const ctx = this[context];
if (host) {
ctx.host = host;
Expand All @@ -187,15 +185,11 @@ function onParseHostnameComplete(flags, protocol, username, password,

function onParsePortComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
this[context].port = port;
}

function onParsePathComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const ctx = this[context];
if (path) {
ctx.path = path;
Expand All @@ -207,16 +201,12 @@ function onParsePathComplete(flags, protocol, username, password,

function onParseSearchComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to make sure I’m not missing something – these are dropped because the on*Complete functions are not called in the case of errors anymore, right?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm also curious about it. Is that why the search setter doesn't need to have onParseError in this case?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the completion and the errors are handled in different callbacks, since it doesn't really make sense to pass all these parsed parts to an error callback anyway.

The setters are ignoring errors per the spec at the moment, so they don't have error callbacks.

const ctx = this[context];
ctx.query = query;
}

function onParseHashComplete(flags, protocol, username, password,
host, port, path, query, fragment) {
if (flags & binding.URL_FLAGS_FAILED)
return;
const ctx = this[context];
if (fragment) {
ctx.fragment = fragment;
Expand Down
52 changes: 31 additions & 21 deletions src/node_url.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1233,7 +1233,8 @@ namespace url {
enum url_parse_state state_override,
Local<Value> base_obj,
Local<Value> context_obj,
Local<Function> cb) {
Local<Function> cb,
Local<Value> error_cb) {
Isolate* isolate = env->isolate();
Local<Context> context = env->context();
HandleScope handle_scope(isolate);
Expand All @@ -1254,20 +1255,19 @@ namespace url {

// Define the return value placeholders
const Local<Value> undef = Undefined(isolate);
Local<Value> argv[9] = {
undef,
undef,
undef,
undef,
undef,
undef,
undef,
undef,
undef,
};

argv[ARG_FLAGS] = Integer::NewFromUnsigned(isolate, url.flags);
if (!(url.flags & URL_FLAGS_FAILED)) {
Local<Value> argv[9] = {
undef,
undef,
undef,
undef,
undef,
undef,
undef,
undef,
undef,
};
argv[ARG_FLAGS] = Integer::NewFromUnsigned(isolate, url.flags);
if (url.flags & URL_FLAGS_HAS_SCHEME)
argv[ARG_PROTOCOL] = OneByteString(isolate, url.scheme.c_str());
if (url.flags & URL_FLAGS_HAS_USERNAME)
Expand All @@ -1284,22 +1284,31 @@ namespace url {
argv[ARG_PORT] = Integer::New(isolate, url.port);
if (url.flags & URL_FLAGS_HAS_PATH)
argv[ARG_PATH] = Copy(env, url.path);
(void)cb->Call(context, recv, arraysize(argv), argv);
} else if (error_cb->IsFunction()) {
Local<Value> argv[2] = { undef, undef };
argv[ERR_ARG_FLAGS] = Integer::NewFromUnsigned(isolate, url.flags);
argv[ERR_ARG_INPUT] =
String::NewFromUtf8(env->isolate(),
input,
v8::NewStringType::kNormal).ToLocalChecked();
(void)error_cb.As<Function>()->Call(context, recv, arraysize(argv), argv);
}

(void)cb->Call(context, recv, 9, argv);
}

static void Parse(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
CHECK_GE(args.Length(), 5);
CHECK(args[0]->IsString());
CHECK(args[2]->IsUndefined() ||
CHECK(args[0]->IsString()); // input
CHECK(args[2]->IsUndefined() || // base context
args[2]->IsNull() ||
args[2]->IsObject());
CHECK(args[3]->IsUndefined() ||
CHECK(args[3]->IsUndefined() || // context
args[3]->IsNull() ||
args[3]->IsObject());
CHECK(args[4]->IsFunction());
CHECK(args[4]->IsFunction()); // complete callback
CHECK(args[5]->IsUndefined() || args[5]->IsFunction()); // error callback

Utf8Value input(env->isolate(), args[0]);
enum url_parse_state state_override = kUnknownState;
if (args[1]->IsNumber()) {
Expand All @@ -1312,7 +1321,8 @@ namespace url {
state_override,
args[2],
args[3],
args[4].As<Function>());
args[4].As<Function>(),
args[5]);
}

static void EncodeAuthSet(const FunctionCallbackInfo<Value>& args) {
Expand Down
10 changes: 10 additions & 0 deletions src/node_url.h
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,10 @@ static inline void PercentDecode(const char* input,
XX(ARG_QUERY) \
XX(ARG_FRAGMENT)

#define ERR_ARGS(XX) \
XX(ERR_ARG_FLAGS) \
XX(ERR_ARG_INPUT) \

static const char kEOL = -1;

enum url_parse_state {
Expand All @@ -484,6 +488,12 @@ enum url_cb_args {
#undef XX
};

enum url_error_cb_args {
#define XX(name) name,
ERR_ARGS(XX)
#undef XX
} url_error_cb_args;

static inline bool IsSpecial(std::string scheme) {
#define XX(name, _) if (scheme == name) return true;
SPECIALS(XX);
Expand Down
31 changes: 23 additions & 8 deletions test/parallel/test-whatwg-url-parsing.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,30 @@ if (!common.hasIntl) {

// Tests below are not from WPT.
const tests = require(path.join(common.fixturesDir, 'url-tests'));
const failureTests = tests.filter((test) => test.failure).concat([
{ input: '' },
{ input: 'test' },
{ input: undefined },
{ input: 0 },
{ input: true },
{ input: false },
{ input: null },
{ input: new Date() },
{ input: new RegExp() },
{ input: () => {} }
]);

for (const test of tests) {
if (typeof test === 'string')
continue;

if (test.failure) {
assert.throws(() => new URL(test.input, test.base),
/^TypeError: Invalid URL$/);
}
for (const test of failureTests) {
assert.throws(
() => new URL(test.input, test.base),
(error) => {
// The input could be processed, so we don't do strict matching here
const match = (error + '').match(/^TypeError: Invalid URL: (.*)$/);
if (!match) {
return false;
}
return error.input === match[1];
});
}

const additional_tests = require(
Expand Down