Skip to content

Add baseUrl option, rename prefix, and allow leading slashes in input #606

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

Open
wants to merge 7 commits into
base: main
Choose a base branch
from

Conversation

sholladay
Copy link
Collaborator

@sholladay sholladay commented Jul 4, 2024

Closes #291

This PR aims to improve the flexibility of input URLs and options for resolving them prior to the request.

  • The prefixUrl option is renamed to prefix to avoid implying that it behaves as a URL with resolution semantics
    • As before, it is prepended to input before any other processing takes place, and it can be a path or full URL with host, if needed.
    • Its primary use case is now to force requests to use a particular path, such as /api, even when input contains a leading slash. In most cases, the new baseUrl option should be used instead for improved flexibility.
  • A new baseUrl option is added which performs URL resolution on the combined prefix + input
    • When this option is used, input (including any prefix) is resolved against the baseUrl to determine the final request URL, according to standard URL resolution rules.
    • Using baseUrl is similar to using the HTML <base> tag or changing the window.location before making the request (except it only applies to Ky). As such, the presence of a trailing slash in the baseUrl and the presence of a leading slash in input (or prefix, if applicable) can affect the final request URL. For example, if input contains a leading slash, it will bypass the path part of the baseUrl, unlike what would happen if the base URL were provided as a prefix. The host will also be bypassed if the input is an absolute URL. This flexibility is what makes baseUrl useful (see prefixUrl is unneededly applied to absolute urls #291).
    • baseUrl can be a path or full URL and it will be resolved against document.baseURI, if necessary.
  • baseUrl and prefix can be used independently or together, they are optional and not mutually exclusive.
  • input can now be provided with or without a leading slash, regardless of which options are used. Previously, using a leading slash in input was not allowed to be combined with a prefix URL. Now, slashes are allowed and automatically normalized so that there will always be a single slash between prefix and input, whether or not prefix ends with a slash or input begins with a slash. However, baseUrl is sensitive to slashes, as explained above. See the table below for examples.

Typical usage

baseUrl prefix input Request URL
'/api/' '' 'users' /api/users
'http://foo.com/api/' '' 'users' http://foo.com/api/users
'http://foo.com/api/' '' 'http://bar.com/other' http://bar.com/other
'' '/api' '/users' /api/users
'' 'http://foo.com/api' '/users' http://foo.com/api/users

Slashes: prefix joins, baseUrl resolves

baseUrl prefix input Request URL
'' 'http://foo.com/api/v2' 'users' http://foo.com/api/v2/users
'' 'http://foo.com/api/v2' '/users' http://foo.com/api/v2/users
'' 'http://foo.com/api/v2/' 'users' http://foo.com/api/v2/users
'' 'http://foo.com/api/v2/' '/users' http://foo.com/api/v2/users
'http://foo.com/api/v2' '' 'users' http://foo.com/api/users
'http://foo.com/api/v2' '' '/users' http://foo.com/users
'http://foo.com/api/v2/' '' 'users' http://foo.com/api/v2/users
'http://foo.com/api/v2/' '' '/users' http://foo.com/users

@faradaytrs
Copy link

i see that it's going to be a breaking change, maybe it's worth about discusssing about the next major? i think it's good to remove default timeout and retry settings, because many people come from fetch and axios and it's not obvious ky have these defaults.

@sholladay
Copy link
Collaborator Author

I would be okay with removing the default timeout, but not the default retries.

@alexgleason
Copy link

baseUrl looks like what I want. I only care about the URL's origin, and also need the ability to pass full URLs. If it works like this, it's perfect:

ky.get('/api/v1/instance', { baseUrl : 'https://ditto.pub' }); // GET https://ditto.pub/api/v1/instance
ky.get('https://ditto.pub/api/v1/timelines/home?max_id=1234', { baseUrl : 'https://ditto.pub' }); // GET https://ditto.pub/api/v1/timelines/home?max_id=1234

In this case, I don't care about a path in the baseUrl. I assume it works like this:

ky.get('/api/v1/instance', { baseUrl : 'https://ditto.pub/@alex' }); // GET https://ditto.pub/api/v1/instance

But I will never pass a baseUrl with a path anyway.

@sholladay
Copy link
Collaborator Author

@alexgleason correct, each of those examples behaves as you described.

I would expect most people to structure their requests like this:

const api = ky.extend({ baseUrl : 'https://ditto.pub/api/v1/' });
api.get('instance'); // GET https://ditto.pub/api/v1/instance
api.get('timelines/home?max_id=1234', { baseUrl : 'https://ditto.pub/api/v1' }); // GET https://ditto.pub/api/v1/timelines/home?max_id=1234
api.get('https://example.com'); // GET https://example.com
api.get('/api/v2/secret'); // GET https://ditto.pub/api/v2/secret

But you could certainly put the api/v1 part in the input instead of at the end of baseUrl, that works, too.

@alexgleason
Copy link

@sholladay I think the way to overcome code complexity and confusion while solving all use-cases is to just support a resolver function like mentioned here: #291 (comment)

But I think it should actually look like this:

const api = ky.create({
  // Accept any `Input` and return any `Input`. Flexible and pure.
  resolver(input: Input): Input {
    // It's not actually complicated to do this yourself either.
    const url = input instanceof Request ? input.url : input.toString();
    return new Request(new URL(url, 'https://ditto.pub'), input);
  }
});

We can keep the prefixUrl for backwards-compatibility, but internally it just creates a resolver, so you can't have both.

const api = ky.create({
  prefixUrl: 'https://ditto.pub', // WARNING: `prefixUrl` is deprecated.
  resolver(input: Input): Input { // ERROR: `prefixUrl` and `resolver` cannot be specified at the same time.
    // ...
  }
});

For improved DX, Ky can include resolvers for a prefix and baseUrl.

import ky from 'ky';

const api = ky.create({
  resolver: ky.prefix('https://ditto.pub/api/v1')
});

const api = ky.create({
  resolver: ky.baseUrl('https://ditto.pub')
});

@sholladay
Copy link
Collaborator Author

@alexgleason the way I am planning to solve that is by allowing users to return a URL instance in a beforeRequest hook. Do you think that is sufficient? It avoids needing to add another option.

@alexgleason
Copy link

@sholladay That could work as long as we have the input there too. In fact that's the only reason it doesn't work already. You don't even need to let it return a URL, you just need to add the input as a parameter.

@alexgleason
Copy link

alexgleason commented Oct 10, 2024

Can it even get to the point of beforeRequest without already having a valid absolute URL though? I don't think it can construct the needed Request object unless you're already specifying a prefixUrl.

It's fine as a hook, but I think it might still have to be a separate hook. beforeInput or something.

@sholladay
Copy link
Collaborator Author

While it doesn't provide input, you can use request.url to see where the request is going to go. That might not suit every use case, though.

@alexgleason
Copy link

@sholladay Currently we can already do that with beforeRequest, but when the request looks like Request { url: "https://ditto.pub/https://ditto.pub/api/v1/timelines/home?max_id=1234" } it's not very helpful. I need the Input so I can join it with a baseUrl. The input might be full URL, or just a path like /api/v1/instance.

@sholladay sholladay marked this pull request as ready for review October 11, 2024 13:51
@sholladay
Copy link
Collaborator Author

sholladay commented Oct 11, 2024

What's the use case that causes you to need a base URL that is unknown at the time of creating the Ky instance? In other words, why is it dynamic?

@alexgleason
Copy link

@sholladay Supporting multiple accounts on Mastodon.

@alexgleason
Copy link

I ended up just writing a custom bare minimum fetch wrapper and accepting that I will have to do await (await ()).json() all over my application code. https://gitlab.com/soapbox-pub/soapbox/-/blob/main/src/api/MastodonClient.ts?ref_type=heads

@sholladay
Copy link
Collaborator Author

@bertho-zero you didn't explain what you prefer about this PR and why.

Your regex is shorter but the current code is more readable and should have slightly better performance. Both are fine, though, and I don't have strong feelings about it.

- After `prefixUrl` and `input` are joined, the result is resolved against the [base URL](https://developer.mozilla.org/en-US/docs/Web/API/Node/baseURI) of the page (if any).
- Leading slashes in `input` are disallowed when using this option to enforce consistency and avoid confusion about how the `input` URL is handled, given that `input` will not follow the normal URL resolution rules when `prefixUrl` is being used, which changes the meaning of a leading slash.
- After `startPath` and `input` are joined, the result is resolved against the [base URL](https://developer.mozilla.org/en-US/docs/Web/API/Node/baseURI) of the page (if any).
- Leading slashes in `input` are disallowed when using this option to enforce consistency and avoid confusion about how the `input` URL is handled, given that `input` will not follow the normal URL resolution rules when `startPath` is being used, which changes the meaning of a leading slash.
Copy link

Choose a reason for hiding this comment

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

Leading slashes in input are disallowed when using this option to enforce consistency and avoid confusion [...]

From what I can tell from the changed code, this is no longer the case?

I personally prefer it that way though and think that we should keep that behaviour

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Indeed. As mentioned in the TODOs section, I haven't gotten around to updating the docs yet, other than a simple find & replace. But thank you for the reminder. 🙂

@bertho-zero
Copy link
Contributor

bertho-zero commented Oct 30, 2024

I like #561 because I don't understand why we should throw an error if the input starts with a slash, I read the topic but still don't find this constraint justified.

I don't like #606 because I can have a prefix like https://domain.com/api/v2 and I don't want to have to parse the prefix to put the host in baseUrl and the path in startPath.

this._input = this._input.slice(1);
}

this._input = this._options.startPath + this._input;
Copy link
Contributor

Choose a reason for hiding this comment

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

I think if _input starts with a protocol (or is a valid URL?) then we should ignore startPath in order to avoid an URL like http://foo.com/http://bar.com

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

That kind of "double URL" construct is useful for reverse proxies. It should be supported.

Also, you would be surprised how hard URL parsing is. Especially for a library like Ky that supports non-browsers.

@sholladay
Copy link
Collaborator Author

I don't like #606 because I can have a prefix like https://domain.com/api/v2 and I don't want to have to parse the prefix to put the host in baseUrl and the path in startPath.

You don't need to split it up like that, you can just put the whole thing in startPath if you always want that host to be used. I realize that may not be entirely obvious. I am open to suggestions for how to make it more obvious. We could just keep the name prefixUrl , then it would be more obvious to me, but that's just too similar to baseUrl, so it creates a new kind of confusion.

@simon-skooster
Copy link

Any news on this PR? I'd love to replace Axios with Ky, but this issue is a blocker.

@sholladay sholladay changed the title Add baseUrl option, rename prefixUrl, and allow leading slashes in input Replace prefixUrl with baseUrl and prefix, allow leading slashes in input Mar 19, 2025
@sholladay sholladay changed the title Replace prefixUrl with baseUrl and prefix, allow leading slashes in input Add baseUrl option, rename prefix, and allow leading slashes in input Mar 19, 2025
@sholladay
Copy link
Collaborator Author

This is almost ready, just need to update the docs and types. Will try to get it landed later this week.

@sholladay
Copy link
Collaborator Author

@sindresorhus this is now ready for review. Currently, it's a breaking change due to the renaming of prefix. We could add an alias to keep backwards compatibility, but I think the major version bump is worth it to help make people aware of these changes, which I suspect will be helpful to many.

Most users will simply need to update the option name, but it's recommended to switch to baseUrl if possible, for improved flexibility (e.g. the ability to bypass the baseUrl with an absolute input URL).

@sholladay sholladay requested a review from sindresorhus April 22, 2025 23:52
@sindresorhus
Copy link
Owner

This looks like a good improvement to me.

I would prefer keeping an alias to not make this change too disruptive. We can add a @deprecated on the docs for prefixUrl in the TS docs, and in the major version after this one, we can hard-deprecate prefixUrl with an error in the actual code.


A prefix to prepend to the `input` URL before making the request. It can be any valid path or URL, either relative or absolute. A trailing slash `/` is optional and will be added automatically, if needed, when it is joined with `input`. Only takes effect when `input` is a string.

Useful when used with [`ky.extend()`](#kyextenddefaultoptions) to create niche-specific Ky-instances.
Copy link
Owner

Choose a reason for hiding this comment

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

Is prefix still preferred for this or should it be moved to baseUrl?

@@ -191,29 +191,55 @@ Search parameters to include in the request URL. Setting this will override all

Accepts any value supported by [`URLSearchParams()`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams).

##### prefixUrl
Copy link
Owner

Choose a reason for hiding this comment

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

We need even clearer docs about the difference between baseUrl and prefix and when to use each. Maybe with some real-world example use-cases. Otherwise, it's going to be confusing for users and that ends up being a support burden for us.

@@ -191,29 +191,55 @@ Search parameters to include in the request URL. Setting this will override all

Accepts any value supported by [`URLSearchParams()`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/URLSearchParams).

##### prefixUrl
##### baseUrl
Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would be great to have a matrix like you added in the original pull request description for both the baseUrl and prefix sections that goes through the motions of showing most (if not all) relevant permutations of baseUrl (or prefix), input and the resulting request URL.

This could help de-mystify what both options do and what might be appropriate for any given use case.

I think that's especially useful because not all people will know that "prefix" and "base URL" mean (sometimes subtly) different things (of course these docs make that clear, but still).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I was originally going to do that but it felt like a lot of information all at once and made the choices seem complex, which could actually be more confusing than helpful. I went with a simple, explicit recommendation to use baseUrl unless you know you need prefix. It seems clearer to me overall.

I think we should see what questions come up and then make tweaks based on that. If there ends up being a lot, we could add a full matrix of examples to the FAQ. What do you think?

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 perfectly fine with that.

My angle is mainly that I think there might be a lot of (potential) users coming from contexts where all they need is setting some API prefix string (e.g. 'https://my-awesome-site.com/api/v1') so that their API client code can be simplified to accept path segments without that prefix (e.g. '/users' or 'users') to produce a simply joined request URL (e.g. 'https://my-awesome-site.com/api/v1/users'). For those cases, developers might never think to care about what difference a leading slash on '/users' vs no leading slash on 'users' could make. For them, both are equivalent and should result in the same request URL.

Since it might never occur to them that there can even be a difference between both cases, they might also just gloss over the documentation for prefix and baseUrl assuming (wrongly) that surely it behaves as expected. A tabular representation of the "concatenation" behavior might be a bit of an easier "catch" while reading the docs.

But again, I'm perfectly fine with not having that explained in a tabular manner.

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.

prefixUrl is unneededly applied to absolute urls
8 participants