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

feat: create axios instance on req.$axios for server-middleware use #428

Closed
wants to merge 4 commits into from

Conversation

rchl
Copy link
Member

@rchl rchl commented Oct 14, 2020

An extra instance of axios is created and attached to "req.$axios" so
that server middlewares have access to the axios instance that has
extensions provided by this module.

The "proxyHeaders" option is ignored for that instance as it would be
both awkward to implement and potentially wrong. This is because
"proxyHeaders" is proxying requests from the request that initiated the
page request while server-middleware instance is kinda independent of
individual page requests.

Resolves #427

@codecov
Copy link

codecov bot commented Oct 14, 2020

Codecov Report

Merging #428 into master will decrease coverage by 37.69%.
The diff coverage is 42.98%.

Impacted file tree graph

@@             Coverage Diff             @@
##           master     #428       +/-   ##
===========================================
- Coverage   95.55%   57.86%   -37.70%     
===========================================
  Files           1        2        +1     
  Lines          45      159      +114     
  Branches       25       53       +28     
===========================================
+ Hits           43       92       +49     
- Misses          2       52       +50     
- Partials        0       15       +15     
Impacted Files Coverage Δ
lib/axios-extend.js 37.50% <37.50%> (ø)
lib/module.js 96.36% <100.00%> (+0.80%) ⬆️

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update a3f708d...72cb792. Read the comment docs.

An extra instance of axios is created and attached to "process.axios" so
that server middlewares have access to the axios instance that has
extensions provided by this module.

The "proxyHeaders" option is ignored for that instance as it would be
both awkward to implement and potentially wrong. This is because
"proxyHeaders" is proxying requests from the request that initiated the
page request while server-middleware instance is kinda independent of
individual page requests.

Also, it's not an entirely perfect solution because that "process.axios"
instance is only extended when axios server-side plugin is executed.
Ideally, it would be extended immediately when the module's code runs.
That would require some refactoring to eliminate templates from the
code that extends the instance and move it to a separate file. This is
doable but would result in fewer byte savings.

Resolves nuxt-community#427
@rchl rchl force-pushed the feat/axios-server-middleware branch from 99194a4 to cbdc9f7 Compare October 14, 2020 20:19
@rchl
Copy link
Member Author

rchl commented Oct 15, 2020

Also, I'm thinking maybe I should use process.$axios instead to make it a bit more unique.

But let me know what you think about this whole idea in general.

lib/module.js Outdated
@@ -121,6 +122,10 @@ function axiosModule (_moduleOptions) {
// Set _AXIOS_BASE_URL_ for dynamic SSR baseURL
process.env._AXIOS_BASE_URL_ = options.baseURL

// Create a separate instance for server middlewares.
// Extended from a server-side plugin.
process.axios = Axios.create({ baseURL: options.baseURL })
Copy link
Member

Choose a reason for hiding this comment

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

We can attach axios instance to nuxt (for other modules) and req object (for server middleware) to avoid leaking references to global

Copy link
Member Author

Choose a reason for hiding this comment

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

To attach to req we would only be able to do it when a request is actually made. This here creates process.axios earlier on purpose so that there is no point during runtime where server middleware would see undefined process.axios.

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm, I think the current solution has a problem that in the first request to server-middleware the axios instance would not be extended yet (since the server-side plugin would run after server middleware).

So that pertains to the second issue you've made below -- we would need to probably extract the "extending" code into a separate file and call it immediately rather than relying on plugin extending it.

Copy link
Member Author

Choose a reason for hiding this comment

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

And then putting in on req would actually be possible from the module and potentially be a better idea. It would mean that the instance of axios would be a bit harder to access (would have to be passed along various calls potentially) but workable.

lib/plugin.js Outdated
@@ -184,6 +185,14 @@ const setupProgress = (axios) => {
axios.defaults.onDownloadProgress = onProgress
}<% } %>

if (process.server) {
Copy link
Member

Choose a reason for hiding this comment

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

Why this change is necessary with this feature?

Copy link
Member Author

Choose a reason for hiding this comment

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

This is to extend the process.axios instance with extra methods and configuration specific to this module.

As I've hinted in the commit message, we could also extract the "extending code" into a separate file and use it both from the module and the plugin but that would require refactoring to remove the use of templates. And that means we couldn't include parts of its code conditionally, based on settings. The whole code would always be included in the bundles.

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 we can write node specific logic to create instance out of the plugin so we have clear control over behavior and also not depending on first SSR request to be done to make instance initialized

Copy link
Member Author

Choose a reason for hiding this comment

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

What do you mean by creating an instance out of the plugin?

Extract the code into a separate file as I've suggested or just write extra code? We basically need this part accessible from the module also:

const axiosExtra = {
setBaseURL (baseURL) {
this.defaults.baseURL = baseURL
},
setHeader (name, value, scopes = 'common') {
for (const scope of Array.isArray(scopes) ? scopes : [ scopes ]) {
if (!value) {
delete this.defaults.headers[scope][name];
return
}
this.defaults.headers[scope][name] = value
}
},
setToken (token, type, scopes = 'common') {
const value = !token ? null : (type ? type + ' ' : '') + token
this.setHeader('Authorization', value, scopes)
},
onRequest(fn) {
this.interceptors.request.use(config => fn(config) || config)
},
onResponse(fn) {
this.interceptors.response.use(response => fn(response) || response)
},
onRequestError(fn) {
this.interceptors.request.use(undefined, error => fn(error) || Promise.reject(error))
},
onResponseError(fn) {
this.interceptors.response.use(undefined, error => fn(error) || Promise.reject(error))
},
onError(fn) {
this.onRequestError(fn)
this.onResponseError(fn)
},
create(options) {
return createAxiosInstance(defu(options, this.defaults))
}
}
// Request helpers ($get, $post, ...)
for (const method of ['request', 'delete', 'get', 'head', 'options', 'post', 'put', 'patch']) {
axiosExtra['$' + method] = function () { return this[method].apply(this, arguments).then(res => res && res.data) }
}
const extendAxiosInstance = axios => {
for (const key in axiosExtra) {
axios[key] = axiosExtra[key].bind(axios)
}
}
const createAxiosInstance = axiosOptions => {
// Create new axios instance
const axios = Axios.create(axiosOptions)
axios.CancelToken = Axios.CancelToken
axios.isCancel = Axios.isCancel
// Extend axios proto
extendAxiosInstance(axios)
// Setup interceptors
<% if (options.debug) { %>setupDebugInterceptor(axios) <% } %>
<% if (options.credentials) { %>setupCredentialsInterceptor(axios)<% } %>
<% if (options.progress) { %>setupProgress(axios) <% } %>
<% if (options.retry) { %>axiosRetry(axios, <%= serialize(options.retry) %>)<% } %>
return axios
}
<% if (options.debug) { %>
const log = (level, ...messages) => console[level]('[Axios]', ...messages)
const setupDebugInterceptor = axios => {
// request
axios.onRequestError(error => {
log('error', 'Request error:', error)
})
// response
axios.onResponseError(error => {
log('error', 'Response error:', error)
})
axios.onResponse(res => {
log(
'info',
'[' + (res.status + ' ' + res.statusText) + ']',
'[' + res.config.method.toUpperCase() + ']',
res.config.url)
if (process.browser) {
console.log(res)
} else {
console.log(JSON.stringify(res.data, undefined, 2))
}
return res
})
}<% } %>
<% if (options.credentials) { %>
const setupCredentialsInterceptor = axios => {
// Send credentials only to relative and API Backend requests
axios.onRequest(config => {
if (config.withCredentials === undefined) {
if (!/^https?:\/\//i.test(config.url) || config.url.indexOf(config.baseURL) === 0) {
config.withCredentials = true
}
}
})
}<% } %>

So are you suggesting to duplicate it or extract to a separate file or something else?

Copy link
Member

Choose a reason for hiding this comment

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

We may refactor useful logic into a shared CJS file as well and use both for module and runtime to avoid duplicate code like this

Copy link
Member Author

@rchl rchl Oct 15, 2020

Choose a reason for hiding this comment

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

Yes. The only concern with that is that means the whole code will be included in the bundles rather than only code that is needed for enabled options. But I guess you are fine with that?

BTW. Is there a specific reason why you are using and suggesting comonjs syntax? I guess ES6 syntax would work with any supported Nuxt & Node version?

Copy link
Member

Choose a reason for hiding this comment

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

ES6 / Module syntax is possible for nuxt modules with esm which is deprecated. We also disable esm support for typescript and test environments. When testing another project that depends on axios module, this can be problematic.

Yes. The only concern with that is that means the whole code will be included in the bundles rather than only code that is needed for enabled options. But I guess you are fine with that?

We can mark cjs file as esm and use pure comment to allow tree-shaking per export but it shouldn't be a big deal for small shared utils.

I think ideally we should start migrating modules to typescript and proper build step to avoid such issues and also share logic in an easier way..

Comment on lines +137 to +140
const runtimeConfig = {
...this.options.publicPrivateConfig,
...this.options.privateRuntimeConfig
}
Copy link
Member Author

@rchl rchl Oct 27, 2020

Choose a reason for hiding this comment

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

Is there a better way to access runtime config from a module (or server middleware since we are in context of one here)? Ideally with resolved values...

@rchl rchl changed the title feat: create axios instance on process.axios for server-middleware use feat: create axios instance on req.$axios for server-middleware use Oct 27, 2020
@rchl
Copy link
Member Author

rchl commented Dec 11, 2020

@pi0 What are your thoughts on this change? Any blocking issues? It's quite a big refactor so it will just keep getting broken by other changes if left unmerged.

@pi0
Copy link
Member

pi0 commented Dec 11, 2020

Hi @rchl honestly more I'm thinking it might be a tricky feature to add and maintain for axios+http. Purpose of axios module is to bind instance to renderContext not API /Config and as you mentioned most of features like proxyHeaders and progressbar integration are unused. Also usage of interceptors might be different.

Only shared parts i see are utilities and base url. Which we may use ohmyfetch for interface similar to $get util and expose $config to either req or global.nuxt.$config

So I think would introduce global instance only with http v2 (or probably @nuxt/fetch) and this way we might have a more standardized way to make requests in config and API contexts.

@rchl
Copy link
Member Author

rchl commented Dec 11, 2020

OK, I'll close this then and wait for something better to come in the future.

BTW. For me, the main use of this would be to define interceptors for server and client in a common config. But I can deal with those in other ways.

@rchl rchl closed this Dec 11, 2020
@pi0
Copy link
Member

pi0 commented Dec 11, 2020

@rchl Sure will keep you updated on early version.

BTW if it is about an error reporter module like sentry, would be pretty much better intercepting without directly depending on one axios/http implementation 😊

@rchl
Copy link
Member Author

rchl commented Dec 11, 2020

BTW if it is about an error reporter module like sentry, would be pretty much better intercepting without directly depending on one axios/http implementation 😊

The main use case for me is to have better logging of failed requests in local development.

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.

Expose axios instance for use in server middleware
2 participants