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

Packages: Replace the apiRequest module with api-fetch module #7566

Merged
merged 14 commits into from
Jul 17, 2018

Conversation

youknowriad
Copy link
Contributor

@youknowriad youknowriad commented Jun 27, 2018

This PR follows the core-js meeting and replaces the wp.apiRequest with a new wp.fetch module not relying on jQuery APIs but using window.fetch

Differences:

  • Returning regular promises
  • If you want to receive the entire response you need to pass parse: false
  • To avoid data processing, you can pass body directly

Remaining tasks

  • Deprecate wp.apiRequest (I'm leaving it for later, we need to settle on the deprecation strategy first)

closes #7488 #7455 #7515

@youknowriad youknowriad added the npm Packages Related to npm packages label Jun 27, 2018
@youknowriad youknowriad self-assigned this Jun 27, 2018
@youknowriad youknowriad changed the title WIP: Packages: Replace the apiRequest module with fetch module Packages: Replace the apiRequest module with fetch module Jun 28, 2018
@youknowriad youknowriad requested a review from a team June 28, 2018 12:30
middlewares.push( middleware );
}

function request( options ) {

Choose a reason for hiding this comment

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

Why is it called request? Does this map to fetch somewhere?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It's just an internal name to avoid confusion with window.fetch (which can also be accessed by fetch since it's a global) used on the same file.

Choose a reason for hiding this comment

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

Maybe it should have been called apifetch then. Or wpfetch.

Copy link
Member

@noisysocks noisysocks left a comment

Choose a reason for hiding this comment

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

Really supportive of this change—moving away from jQuery is 💯.

There's a few places where we're treating response.json() as a promise which will cause errors. For example, the latest posts block doesn't load when I check out this branch. We're also referencing responseJSON in quite a few error handlers which isn't right. (I haven't tested this, though.)

As a developer that's familiar with fetch, I find it a little confusing that @wordpress/fetch doesn't behave exactly the same as window.fetch unless you pass it parse: false. Maybe renaming it to apiFetch would help with this? Alternatively, we could move the "magic" that we do (JSON parsing and response status error handling) into a function that works on regular Response objects.

return {
body,
headers: getResponseHeaders( xhr ),
body: response.json(),
Copy link
Member

Choose a reason for hiding this comment

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

response.json() returns a promise. We'll need to wait for it to resolve here.

https://developer.mozilla.org/en-US/docs/Web/API/Body/json

.catch( ( response ) => {
const failResponse = {
error: true,
errorMsg: response.json().message || __( 'Unknown error' ),
Copy link
Member

Choose a reason for hiding this comment

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

response.json() returns a promise which means message here will be undefined.

https://developer.mozilla.org/en-US/docs/Web/API/Body/json

@@ -302,7 +302,7 @@ export default {
const { postId } = action;
const basePath = wp.api.getPostTypeRoute( getCurrentPostType( getState() ) );
dispatch( removeNotice( TRASH_POST_NOTICE_ID ) );
apiRequest( { path: `/wp/v2/${ basePath }/${ postId }`, method: 'DELETE' } ).then(
fetch( { path: `/wp/v2/${ basePath }/${ postId }`, method: 'DELETE' } ).then(
Copy link
Member

Choose a reason for hiding this comment

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

In the error handler for this request (line 317) we reference err.responseJSON which will now be undefined. We do this in most places that used apiRequest.

@youknowriad
Copy link
Contributor Author

As a developer that's familiar with fetch, I find it a little confusing that @wordpress/fetch doesn't behave exactly the same as window.fetch unless you pass it parse: false. Maybe renaming it to apiFetch would help with this? Alternatively, we could move the "magic" that we do (JSON parsing and response status error handling) into a function that works on regular Response objects.

I guess I like the auto parsing to avoid repeating the same thing over and over again in all of our implementations. I think parsing is the default in 99% of use-cases, isn't it?

@tofumatt
Copy link
Member

If it won't behave like fetch I think putting fetch in the name at all might be best avoided, but apiFetch is fine. Re-using apiRequest is better I think.

I definitely think we should be parsing by default; it's totally the standard use-case and it's handy to not have to worry about it! 👍

@youknowriad
Copy link
Contributor Author

youknowriad commented Jun 29, 2018

We can't reuse apiRequest for backwards compatibility. We can do apiFetch but I personally prefer just fetch and even without parsing the API is different. window.fetch expects two arguments while wp.fetch expects the path as part of the options, do you think we should change this?

Edit Actually, we can't change this because of the way we handle endpoint/namespace support. An option would be to drop this middleware as well though.

Thinking about this more, I think we should keep the single argument signature because it's better for middleware support.

@tofumatt
Copy link
Member

The API is fine to me, but I think using fetch is confusing. It's for API requests and calling it fetch just invites confusion to me. I think avoiding confusion with fetch is worth it (yes, I know the API is different but for newer JS developers this could be confusing, and it's also just a bit strange to scan–also folks might assume our fetch is a shim rather than a different API). Also calling it apiFetch makes the purpose of the package clearer.

@youknowriad
Copy link
Contributor Author

I'm convinced :) updating.

@youknowriad youknowriad changed the title Packages: Replace the apiRequest module with fetch module Packages: Replace the apiRequest module with api-fetch module Jun 29, 2018
};
return response.json()
.then( getErrorFromBody )
.catch( () => getErrorFromBody() );
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if it would be more useful to have this utility function within the apiFetch package itself?

} );
```

The `api-fetch` package provides built-in middlewares you can use to provide a `nonce` and a custom `rootURL`.
Copy link
Contributor

Choose a reason for hiding this comment

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

Should the preloading and namespace-endpoint middlewares be mentioned here too?

Copy link
Member

Choose a reason for hiding this comment

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

Also, how does one leverage said nonce and rootURL middlewares ?

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

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

.then( ( response ) => {
return response.json().then( ( body ) => ( {
body: body,
headers: response.headers,
Copy link
Member

Choose a reason for hiding this comment

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

What is the shape of response.headers here ? Is it an object implementing the Headers interface ? That doesn't seem compatible with how this was previously shaped as a set of tuples.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

unless I'm missing something, I don't see it being used or exposed anywhere. I think it should be fine to remove entirely.

method: 'POST',
data: { name: termName },
} ).catch( ( response ) =>
response.json().then( ( body ) => {
Copy link
Member

Choose a reason for hiding this comment

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

So in .then we receive a JSON payload by default ? But .catch we receive response always ? And in .then we have the option to receive response by passing parse: false ? Feels like an inconsistent developer experience.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, what's the alternative you suggest? Try to parse the response in errors as well?

Copy link
Member

Choose a reason for hiding this comment

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

That, or just abandon the auto-parse convenience. Consistency could have the bigger benefit.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I personally prefer to keep the auto-parsing because of the duplication it can create. I'm fine with consistency between then/catch

Copy link
Member

Choose a reason for hiding this comment

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

I think parsing error response bodies makes sense since this package is specifically designed to work with the WP REST API, which we know returns nicely formatted error responses:

{
    "code": "rest_forbidden",
    "data": {
        "status": 401
    },
    "message": "Sorry, you are not allowed to do that."
}

Most error handling can then be as simple as:

apiFetch( ... ).catch( ( { message } ) => {
	console.error( message );
} );

If we cannot parse the error response body (e.g. because of a connection issue), then we should throw a similarly shaped object, e.g.:

{
    code: "unknown_error",
    message: "An unknown error occurred.",
}

} );
```

The `api-fetch` package provides built-in middlewares you can use to provide a `nonce` and a custom `rootURL`.
Copy link
Member

Choose a reason for hiding this comment

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

Also, how does one leverage said nonce and rootURL middlewares ?

namespaceEndpointMiddleware,
httpV1Middleware,
raw,
].reverse();
Copy link
Member

Choose a reason for hiding this comment

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

I guess this is inherited, but why do we go through the effort to Array#reverse if we could already know what the result will be in assigning this array? i.e.

const steps = [
	raw,
	httpV1Middleware,
	namespaceEndpointMiddleware,
	...middlewares,
];

.then( parseResponse )
.catch( ( response ) => {
if ( ! parse ) {
Promise.reject( response );
Copy link
Member

Choose a reason for hiding this comment

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

We're missing a return here.

@youknowriad youknowriad force-pushed the try/fetch-package branch 2 times, most recently from 8783df9 to 0fd300e Compare July 17, 2018 09:47
@youknowriad
Copy link
Contributor Author

This should be ready to land if I can get another review :)

Copy link
Member

@tofumatt tofumatt left a comment

Choose a reason for hiding this comment

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

I think the helper functions used in the apiFetch promise chain should be defined outside the function and I think there are some little cleanup things that need to happen here.

reject( xhr );
} );
const basePath = wp.api.getTaxonomyRoute( this.props.slug );
// Tries to create a term or fetch it if it already exists
Copy link
Member

Choose a reason for hiding this comment

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

Wild nitpick but comments should end in a period.

this.addRequest = apiFetch( {
path: `/wp/v2/${ basePath }?${ stringify( { ...DEFAULT_QUERY, search: termName } ) }`,
} );
return this.addRequest.then( ( searchResult ) => {
Copy link
Member

Choose a reason for hiding this comment

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

I guess since we're returning from inside the catch call it's tough to chain this nicer but returning a promise's chained return value is a bit hard to parse/read/grok. Not sure how to make it nicer without async/await syntax which is a whole refactor though 🤷‍♂️

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I find that using async/await is not necessarily better when we use .catch because you end up with await inside try/catch and you also have to embed try/catch inside try/catch sometimes.

} ).catch( ( error ) => {
const errorCode = error.code;
if ( errorCode === 'term_exists' ) {
// search the new category created since last fetch
Copy link
Member

Choose a reason for hiding this comment

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

This should be a full sentence 😄

request
.then(
( newPost ) => {
const reset = isAutosave ? resetAutosave : resetPost;
Copy link
Member

Choose a reason for hiding this comment

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

This is pretty aggressive whitespace, any chance we could tighten it up a bit?

type: 'SAVE_SHARED_BLOCK_FAILURE',
id: 123,
} );
done();
Copy link
Member

Choose a reason for hiding this comment

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

Maybe I missed it in a previous review, but why the move to using done here over returning the Promise?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It was not possible on the initial implementation (I guess because of jQuery), I'll see if it's better now.

headers,
}
);
const checkStatus = ( response ) => {
Copy link
Member

Choose a reason for hiding this comment

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

It seems odd to be that these are all defined as inline functions inside the function itself. Won't that mean they're instantiated every time the function is called? Why not have these all exist outside the function itself? They're already stateless/functional and it would improve readability of the function (you only have to follow the logic of the function over the definitions as well) and it might improve performance by avoiding re-creating them on each call.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

parseResponse is not stateless, and I think creating inline function is not bad in terms of performance so I preferred consistency.

Copy link
Member

Choose a reason for hiding this comment

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

Oh shoot, I missed that.

That makes sense; I find inline functions make a function a lot tougher to understand right away, but that's cool.

@@ -32,15 +27,12 @@ const createPreloadingMiddleware = ( preloadedData ) => ( options, next ) => {
.join( '&' );
}

if ( typeof options.path === 'string' ) {
if ( typeof options.path === 'string' && options.parse !== false ) {
Copy link
Member

Choose a reason for hiding this comment

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

"Does not equal false" is harder for my brain (and probably other brains) to grok than "is true". Any reason not to just use options.parse or options.parse === true if you want to be strict about it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The idea is that it's clearer this way that it's opt-out and not opt-in. The default value allows us to change the condition but I think it's a little bit safer to check against false values on "opt-out" flags.

Copy link
Member

Choose a reason for hiding this comment

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

Right, you need to pass parse: false to opt-out, that makes sense. But why not make the condition check if ( typeof options.path === 'string' && options.parse ) {?

I just find it easier to read that we're checking for true rather than for "not false".

Copy link
Contributor Author

Choose a reason for hiding this comment

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

👍 updating

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Noting that if options.parse is undefined options.parse returns false but in the current condition it returns true so we have to assign a default value before simplifying the check.

Copy link
Member

Choose a reason for hiding this comment

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

Oh yeah, sorry, I should have said that; it was implied but I should've mentioned it explicitly. My bad 😞

I think that'll make the code easier to follow and the intent clearer as well. 👍

@@ -20,9 +20,8 @@ describe( 'Preloading Middleware', () => {
};

const response = prelooadingMiddleware( requestOptions );
response.done( ( value ) => {
return response.then( ( value ) => {
Copy link
Member

Choose a reason for hiding this comment

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

And here you moved to returning the Promise, so I'm not sure why you went away from it before 😄

Copy link
Member

@tofumatt tofumatt left a comment

Choose a reason for hiding this comment

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

Makes sense! I left a note about options.parse just because I think the conditional is weird and I'm still not huge on inline functions for readability. I'd be curious about the performance difference for such a frequently-used function. 🤔

Anyway, looks good!

headers,
}
);
const checkStatus = ( response ) => {
Copy link
Member

Choose a reason for hiding this comment

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

Oh shoot, I missed that.

That makes sense; I find inline functions make a function a lot tougher to understand right away, but that's cool.

@@ -32,15 +27,12 @@ const createPreloadingMiddleware = ( preloadedData ) => ( options, next ) => {
.join( '&' );
}

if ( typeof options.path === 'string' ) {
if ( typeof options.path === 'string' && options.parse !== false ) {
Copy link
Member

Choose a reason for hiding this comment

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

Right, you need to pass parse: false to opt-out, that makes sense. But why not make the condition check if ( typeof options.path === 'string' && options.parse ) {?

I just find it easier to read that we're checking for true rather than for "not false".

@youknowriad youknowriad merged commit 247a6e0 into master Jul 17, 2018
@youknowriad youknowriad deleted the try/fetch-package branch July 17, 2018 11:52
@youknowriad
Copy link
Contributor Author

Thanks all for the reviews.

@youknowriad
Copy link
Contributor Author

What do we do about https://www.npmjs.com/package/@wordpress/api-request ?

We should probably just add a "deprecated" notice manually to the npm README

@gziolo
Copy link
Member

gziolo commented Jul 17, 2018

@ntwb any hints, how we should proceed?

@aduth
Copy link
Member

aduth commented Jul 17, 2018

Running npm run docs:build on master after this merge produces some local changes:

diff --git a/docs/manifest.json b/docs/manifest.json
index 3efafeb88..66ae6255e 100644
--- a/docs/manifest.json
+++ b/docs/manifest.json
@@ -222,9 +222,9 @@
 		"parent": "packages"
 	},
 	{
-		"title": "@wordpress/api-request",
-		"slug": "packages-api-request",
-		"markdown_source": "https://github.com/raw/WordPress/gutenberg/master/packages/api-request/README.md",
+		"title": "@wordpress/api-fetch",
+		"slug": "packages-api-fetch",
+		"markdown_source": "https://github.com/raw/WordPress/gutenberg/master/packages/api-fetch/README.md",
 		"parent": "packages"
 	},
 	{

@youknowriad
Copy link
Contributor Author

good catch @aduth forgot this one :)

@ntwb
Copy link
Member

ntwb commented Jul 18, 2018

What do we do about npmjs.com/package/@wordpress/api-request ?

We should probably just add a "deprecated" notice manually to the npm README

@ntwb any hints, how we should proceed?

I've just deprecated @wordpress/api-request on npmjs.com

If someone installs it they will see a notice:

"This package is deprecated, use @wordpress/api-fetch instead."

@ntwb
Copy link
Member

ntwb commented Jul 18, 2018

cc @gziolo @youknowriad ☝️

@gziolo
Copy link
Member

gziolo commented Jul 18, 2018

Awesome, thanks!

svn2github pushed a commit to svn2github/wordcamp-mu-plugins that referenced this pull request Jul 19, 2018
Starting in Gutenberg 3.1, the 'wp-api-request' script gets de-registered
and re-registered with a Gutenberg-specific version. Unfortunately, the
script doesn't also get re-localized with the `wpApiSettings` variable that's
needed for wp-api.js, unless the current screen contains an editor instance.

The bug was reported here:
WordPress/gutenberg#7488

And it should be fixed once the changes in this pull request get released:
WordPress/gutenberg#7566

In the meantime, this hotfix should ensure that `wpApiSettings` is available
regardless of the presence of an editor instance.



git-svn-id: https://meta.svn.wordpress.org/sites/trunk/wordcamp.org/public_html/wp-content/mu-plugins@7476 74240141-8908-4e6f-9713-ba540dce6ec7
@@ -0,0 +1,64 @@
# @wordpress/api-fetch

Wrapper around `window.fetch` to call WordPress REST APIs.
Copy link
Member

Choose a reason for hiding this comment

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

Minor: Should we maybe be more explicit for general use that this requires Fetch (and thus, a polyfill maybe required for older browsers?)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, definitely, this makes me think we may need a consistent way to gather the required polyfills for each package. Better automated but It's probably not easy to automate.

Copy link
Member

Choose a reason for hiding this comment

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

Related: #7159

iandunn pushed a commit to WordPress/wordcamp.org that referenced this pull request Nov 15, 2018
Starting in Gutenberg 3.1, the 'wp-api-request' script gets de-registered
and re-registered with a Gutenberg-specific version. Unfortunately, the
script doesn't also get re-localized with the `wpApiSettings` variable that's
needed for wp-api.js, unless the current screen contains an editor instance.

The bug was reported here:
WordPress/gutenberg#7488

And it should be fixed once the changes in this pull request get released:
WordPress/gutenberg#7566

In the meantime, this hotfix should ensure that `wpApiSettings` is available
regardless of the presence of an editor instance.



git-svn-id: https://meta.svn.wordpress.org/sites/trunk/wordcamp.org@7476 74240141-8908-4e6f-9713-ba540dce6ec7
vedanshujain pushed a commit to WordPress/wordcamp.org that referenced this pull request Nov 29, 2018
Starting in Gutenberg 3.1, the 'wp-api-request' script gets de-registered
and re-registered with a Gutenberg-specific version. Unfortunately, the
script doesn't also get re-localized with the `wpApiSettings` variable that's
needed for wp-api.js, unless the current screen contains an editor instance.

The bug was reported here:
WordPress/gutenberg#7488

And it should be fixed once the changes in this pull request get released:
WordPress/gutenberg#7566

In the meantime, this hotfix should ensure that `wpApiSettings` is available
regardless of the presence of an editor instance.



git-svn-id: https://meta.svn.wordpress.org/sites/trunk/wordcamp.org@7476 74240141-8908-4e6f-9713-ba540dce6ec7


git-svn-id: https://github.com/WordPress/wordcamp.org.git@1266 a64dc54a-4538-25e2-d9ce-ce57b7db7bff
vedanshujain pushed a commit to WordPress/wordcamp.org that referenced this pull request Nov 29, 2018
Starting in Gutenberg 3.1, the 'wp-api-request' script gets de-registered
and re-registered with a Gutenberg-specific version. Unfortunately, the
script doesn't also get re-localized with the `wpApiSettings` variable that's
needed for wp-api.js, unless the current screen contains an editor instance.

The bug was reported here:
WordPress/gutenberg#7488

And it should be fixed once the changes in this pull request get released:
WordPress/gutenberg#7566

In the meantime, this hotfix should ensure that `wpApiSettings` is available
regardless of the presence of an editor instance.



git-svn-id: http://meta.svn.wordpress.org/sites/trunk/wordcamp.org@7476 74240141-8908-4e6f-9713-ba540dce6ec7
vedanshujain pushed a commit to WordPress/wordcamp.org that referenced this pull request Dec 6, 2018
Starting in Gutenberg 3.1, the 'wp-api-request' script gets de-registered
and re-registered with a Gutenberg-specific version. Unfortunately, the
script doesn't also get re-localized with the `wpApiSettings` variable that's
needed for wp-api.js, unless the current screen contains an editor instance.

The bug was reported here:
WordPress/gutenberg#7488

And it should be fixed once the changes in this pull request get released:
WordPress/gutenberg#7566

In the meantime, this hotfix should ensure that `wpApiSettings` is available
regardless of the presence of an editor instance.



git-svn-id: https://meta.svn.wordpress.org/sites/trunk/wordcamp.org@7476 74240141-8908-4e6f-9713-ba540dce6ec7
vedanshujain pushed a commit to WordPress/wordcamp.org that referenced this pull request Dec 6, 2018
Starting in Gutenberg 3.1, the 'wp-api-request' script gets de-registered
and re-registered with a Gutenberg-specific version. Unfortunately, the
script doesn't also get re-localized with the `wpApiSettings` variable that's
needed for wp-api.js, unless the current screen contains an editor instance.

The bug was reported here:
WordPress/gutenberg#7488

And it should be fixed once the changes in this pull request get released:
WordPress/gutenberg#7566

In the meantime, this hotfix should ensure that `wpApiSettings` is available
regardless of the presence of an editor instance.



git-svn-id: https://meta.svn.wordpress.org/sites/trunk/wordcamp.org@7476 74240141-8908-4e6f-9713-ba540dce6ec7
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
npm Packages Related to npm packages
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Gutenberg 3.1 breaks Real Media Library (on Wordpress Media Library screen)
8 participants