Show API error in notification - react-admin

I'm missing something basic in the docs. When I get an API validation error, I'm returning a status code and message. It appears that React-Admin is translating the status code to a generic HTTP error code.
My error response.
{"error":
{"statusCode":422,
"name":"Error",
"message":"User with same first and last name already on team."}
}
When my API response with that response, I'm seeing "Unprocessable Entity" in the notification box. I'm using SimpleForm.
I know the status code is being recognized because I've changed the 422 and it shows the corresponding HTTP error description.
In the docs it says to throw and error in your data provider. I've moved that the Simple Rest data provider into my project and have tried throwing errors are various places, but nothing changes on the client.
https://marmelab.com/react-admin/DataProviders.html#error-format
If you have customized error from your API, I'd appreciated any hints you can give. Thx.

Here is the actual error processing:
When a fetch is triggered (usually coming from the data provider), if an error happen, it is caught and transformed into an HttpError and re-thrown (source)
In the process, the HTTP Error message becomes either the json.message or the response statusText. It's here that a 422 HTTP Error becomes Unprocessable Entity (source)
Then the error is caught again at a higher level to be transformed into a redux action. (source)
Finally, the error is transformed into a notification containing the error message.
So, in order to customize your error message, you can easily do that from your custom provider by catching the error in the first place, customizing the error message, and send it again:
const dataProvider = (type, resource, params) => new Promise((resolve, reject) => {
if (type === 'GET_LIST' && resource === 'posts') {
return fetch(...args)
.then(res => res.json())
.then(json => {
if (json.error) {
// The notification will show what's in { error: "message" }
reject(new Error(json.error.message));
return;
}
resolve(json);
});
}
// ...
});

in Backend, I structure the response as
res.json({status:400,message:"Email Address is invalid!"})
In Client side, modify the convertHTTPResponse in dataprovider as:
const convertHTTPResponse = (response, type, resource, params) => {
const { headers, json } = response;
switch (type) {
case GET_LIST:
case GET_MANY_REFERENCE:
if(json.status === 200){
if (!headers.has('content-range')) {
throw new Error('The Content-Range header is missing in the HTTP Response.);
}
return {
data: json.docs,
total: parseInt(
headers
.get('content-range')
.split('/')
.pop(),
10
),
};
}else{
throw new Error(json.message)
}
default:
if(json.status === 200){
return { data: json.docs };
}else{
throw new Error(json.message)
}
}

Related

Not being able to read statusCode when sending a request

I am developing an api with node.js but i am having trouble with one of my routers while sending a request in postman.
The following is my router:
//#route GET api/profile/github/:username
//#desc Get user repos from github
//#access public
router.get('/github/:username', (req,res)=>{
try {
const options = {
uri: `https://api/github.com/users/${req.params.username}/repos?per_page=5&sort=created:asc&client_id=${config.get('githubClientId')}&client_secret=${config.get('githubSecret')}`,
method:'GET',
headers:{'user_agent': 'node.js'}
};
request(options, (error, response, body) => {
if(error) console.error(error);
if(response.statusCode !== 200){
res.status(404).json('No Github profile found');
}
res.json(JSON.parse(body));
});
} catch (error) {
console.error(error.message);
res.status(500).send('Server Error');
}
})
So in this route i am trying to find a github username that is being passed via the uri.
This is the request i am sending:
http://localhost:5000/api/profile/github/<GITHUB_USERNAME>
but when i send my request i get the following error in my VSC console.
Cannot read properties of undefined (reading 'statusCode')
if(response.statusCode !==200){
There are a couple issues here:
First, https://api/github.com/users/xxx should be https://api.github.com/users/xxx. You put a / where there should have been a . .
And, in fact, your very code should be showing the error:
Error: getaddrinfo ENOTFOUND api
at GetAddrInfoReqWrap.onlookup [as oncomplete] (node:dns:71:26) {
errno: -3008,
code: 'ENOTFOUND',
syscall: 'getaddrinfo',
hostname: 'api'
}
which is telling you that the domain api was not found. It's looking for that domain because of the error in your URL.
Second, this error is complicated in your code because if you get an error returned from the request() library, then the callback arguments response and body are invalid, but you try to use them. The physical request did not succeed so there is no response or body.
You can amend your code like this:
router.get('/github/:username', (req, res) => {
try {
const options = {
uri: `https://api/github.com/users/${req.params.username}/repos?per_page=5&sort=created:asc&client_id=${config.get('githubClientId')}&client_secret=${config.get('githubSecret')}`,
method: 'GET',
headers: { 'user_agent': 'node.js' }
};
request(options, (error, response, body) => {
if (error) {
console.error(error);
res.status(500).json({code: error.code, message: "Network error communicating with github"});
return;
}
if (response.statusCode !== 200) {
res.status(404).json('No Github profile found');
return;
}
res.json(JSON.parse(body));
});
} catch (error) {
console.error(error.message);
res.status(500).send('Server Error');
}
});
This handles the error by returning a 500 status and creating an error object to send back to the client. It also adds return statements after sending a response so the following code doesn't execute and try to send other responses.
NOTE: The request() library has been deprecated since early 2020 and will no longer be developed with new features. It is NOT recommended for new code. You can use newer, promise-based libraries such as node-fetch, got or any of the others listed here. In the most recent versions of nodejs, you can use the built-in version of fetch.

Handling an authentication page returned by an axios request in vue

I have a vue app that sits behind a firewall, which controls authentication. When you first access the app you need to authenticate after which you can access the app and all is well until the authentication expires. From the point of view of my app I only know that the user needs to re-authenticate when I use axios to send off an API request and instead of the expected payload I receive a 403 error, which I catch with something like the following:
import axios from 'axios'
var api_url = '...'
export default new class APICall {
constructor() {
this.axios = axios.create({
headers: {
'Accept': 'application/json',
'Cache-Control': 'no-cache',
'Content-Type': 'application/json',
},
withCredentials: true,
baseURL: api_url
});
}
// send a get request to the API with the attached data
GET(command) {
return this.axios.get(command)
.then((response) => {
if (response && response.status === 200) {
return response.data; // all good
} else {
return response; // should never happen
}
}).catch((err) => {
if (err.message
&& err.message=="Request failed with status code 403"
&& err.response && err.response.data) {
// err.response.data now contains HTML for the authentication page
// and successful authentication on this page resends the
// original axios request, which is in err.response.config
}
})
}
}
Inside the catch statement, err.response.data is the HTML for the authentication page and successfully authenticating on this page automatically re-fires the original request but I can't for the life of me see how to use this to return the payload I want to my app.
Although it is not ideal from a security standpoint, I can display the content of err.response.data using a v-html tag when I do this I cannot figure out how to catch the payload that comes back when the original request is fired by the authentication page, so the payload ends up being displayed in the browser. Does anyone know how to do this? I have tried wrapping everything inside promises but I think that the problem is that I have not put a promise around the re-fired request, as I don't have direct control of it.
Do I need to hack the form in err.response.data to control how the data is returned? I get the feeling I should be using an interceptor but am not entirely sure how they work...
EDIT
I have realised that the cleanest approach is to open the form in error.response.data in a new window, so that the user can re-authenticate, using something like:
var login_window = window.open('about:blank', '_blank');
login_window.document.write(error.response.data)
Upon successful re-authentication the login_window now contains the json for the original axios get request. So my problem now becomes how to detect when the authentication fires and login_window contains the json that I want. As noted in Detect form submission on a page, extracting the json from the formatting window is also problematic as when I look at login_window.document.body.innerText "by hand" I see a text string of the form
JSON
Raw Data
Headers
Save
Copy
Collapse All
Expand All
status \"OK\"
message \"\"
user \"andrew\"
but I would be happy if there was a robust way of determining when the user submits the login form on the page login_window, after which I can resend the request.
I would take a different approach, which depends on your control over the API:
Option 1: you can control (or wrap) the API
have the API return 401 (Unauthorized - meaning needs to authenticate) rather than 403 (Forbidden - meaning does not have appropriate access)
create an authentication REST API (e.g. POST https://apiserver/auth) which returns a new authentication token
Use an Axios interceptor:
this.axios.interceptors.response.use(function onResponse(response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// no need to do anything here
return response;
}, async function onResponseError(error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
if ("response" in error && "config" in error) { // this is an axios error
if (error.response.status !== 401) { // can't handle
return error;
}
this.token = await this.axios.post("auth", credentials);
error.config.headers.authorization = `Bearer ${this.token}`;
return this.axios.request(config);
}
return error; // not an axios error, can't handler
});
The result of this is that the user does not experience this at all and everything continues as usual.
Option 2: you cannot control (or wrap) the API
use an interceptor:
this.axios.interceptors.response.use(function onResponse(response) {
// Any status code that lie within the range of 2xx cause this function to trigger
// no need to do anything here
return response;
}, async function onResponseError(error) {
// Any status codes that falls outside the range of 2xx cause this function to trigger
if ("response" in error && "config" in error) { // this is an axios error
if (error.response.status !== 403) { // can't handle
return error;
}
if (!verifyLoginHtml(error.response.data)) { // this is not a known login page
return error;
}
const res = await this.axios.post(loginUrl, loginFormData);
return res.data; // this should be the response to the original request (as mentioned above)
}
return error; // not an axios error, can't handler
});
One solution is to override the <form>'s submit-event handler, and then use Axios to submit the form, which gives you access to the form's response data.
Steps:
Query the form's container for the <form> element:
// <div ref="container" v-html="formHtml">
const form = this.$refs.container.querySelector('form')
Add a submit-event handler that calls Event.preventDefault() to stop the submission:
form.addEventListener('submit', e => {
e.preventDefault()
})
Use Axios to send the original request, adding your own response handler to get the resulting data:
form.addEventListener('submit', e => {
e.preventDefault()
axios({
method: form.method,
url: form.action,
data: new FormData(form)
})
.then(response => {
const { data } = response
// data now contains the response of your original request before authentication
})
})
demo

Using Mongoose pre save hook result in: Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client

I am trying to hook into the save function in Mongoose to return an error to the client of a REST API if a certain condition is not met in the schema. I can't use a validator for this as the restriction is calculated over multiple fields of the schema.
I am trying to add a hook in the following style:
mySchema.pre('save', function (next) {
if(condition_is_not_met) {
const err = new Error('Condition was not met');
next(err);
}
next();
});
This throws an error when I try to make a call to the endpoint trying to insert an object that violates the condition checked for in the hook:
Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent
to the client
I am guessing that this happens because execution continues on the route writing the header to send it to the client.
router.post('/mySchema', returnType, (req, res) => {
const s = new mySchema(req.body);
s.save((err) => {
if (err) {
const msg = { message: 'Could not add', error: err }; // This is returned to the caller
res.status(500);
res.send(msg);
}
res.status(200);
res.send(s);
});
});
How can i fix this issue? I have been searching quite a bit but the topics I found so far do not help me to solve my issue. They only helped me to identify the cause without offering a working solution.
did you try having an else branch for the success response? Since even if the object is invalid the success response will still be executed. Try it like below
router.post("/mySchema", returnType, (req, res) => {
const s = new mySchema(req.body);
s.save(err => {
if (err) {
const msg = { message: "Could not add", error: err };
res.status(500);
res.send(msg);
} else {
res.status(200);
res.send(s);
}
});
});
Pardon my code formatting, I am AFK

Unable to get error message from API Angular 6

I use the following function to Post a object of a given class.
public Post<T>(object: T, url: string, httpOptions: {}): Observable<T> {
return this.httpClient.post<T>(`${environment.apiEndpoint}` + url, object, httpOptions)
.pipe(
catchError(this.handleError)
);
}
This function is called in all the service that wants to post something. Like this.
public addEquipment(equipment: Equipment): Observable<Equipment> {
return this.shared.Post<Equipment>(equipment, this.url, this.header);
}
addEquipment is then executed within the component that uses that service. Like this.
this.equipmentService.addEquipment(result)
.subscribe((data: any) => { this.alertService.success(data) }, (error: any) => this.alertService.error(error));
The problem is when the API returns a error (that I can see includes a error message, in the network tab) it tells me that there is no body in the response. The API returns a HttpResult where the error message is added to the response field.
return new HttpResult { StatusCode = HttpStatusCode.Conflict, Response = "Error message"}
I use the following function to handle the errors.
private handleError(error: HttpErrorResponse) {
if (error.error instanceof ErrorEvent) {
// A client-side or network error occurred. Handle it accordingly.
console.error('An error occurred:', error.error.message);
}
else {
console.log(error);
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
console.log(error);
return throwError(
error.error)
};
It is Angular 6 and a ServiceStack API.
All suggestions would be appreciated.
FYI it's preferable to return structured error responses in ServiceStack which you can do with:
HttpError.Conflict("Error message");
Which will let you catch it when using ServiceStack's TypeScript ServiceClient with:
try {
var response = await client.post(request);
} catch (e) {
console.log(e.responseStatus.message);
}
But from this answer for handling errors with Angular HTTP Client it suggests the error body should be accessible with:
this.httpClient
.get("data-url")
.catch((err: HttpErrorResponse) => {
// simple logging, but you can do a lot more, see below
console.error('An error occurred:', err.error);
});

How to customize the authorization error produced by OpenIddict?

I'm using OpenIddict for auth in a .NET Core 2 API. Client side I'm relying on any API errors to follow a custom scheme. However, when e.g. a refresh token has been outdated, I can't seem to find out how to customize the error sent back.
The /token endpoint is never reached, so the error is not under "my control".
The result of the request is a status code 400, with the following JSON:
{"error":"invalid_grant","error_description":"The specified refresh token is no longer valid."}
I've tried to use a custom middleware to catch all status codes (which it does), but the result is returned before the execution of my custom middleware has completed.
How can I properly customize the error or intercept to change it? Thanks!
You can use OpenIddict's event model to customize the token response payloads before they are written to the response stream. Here's an example:
MyApplyTokenResponseHandler.cs
public class MyApplyTokenResponseHandler : IOpenIddictServerEventHandler<ApplyTokenResponseContext>
{
public ValueTask HandleAsync(ApplyTokenResponseContext context)
{
var response = context.Response;
if (string.Equals(response.Error, OpenIddictConstants.Errors.InvalidGrant, StringComparison.Ordinal) &&
!string.IsNullOrEmpty(response.ErrorDescription))
{
response.ErrorDescription = "Your customized error";
}
return default;
}
}
Startup.cs
services.AddOpenIddict()
.AddCore(options =>
{
// ...
})
.AddServer(options =>
{
// ...
options.AddEventHandler<ApplyTokenResponseContext>(builder =>
builder.UseSingletonHandler<MyApplyTokenResponseHandler>());
})
.AddValidation();
The /token endpoint is never reached, so the error is not under "my control".
In fact ,the /token is reached, and the parameter of grant_type equals refresh_token. But the rejection logic when refresh token expired is not processed by us. It is some kind of "hardcoded" in source code :
if (token == null)
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
return;
}
if (options.UseRollingTokens || context.Request.IsAuthorizationCodeGrantType())
{
if (!await TryRedeemTokenAsync(token))
{
context.Reject(
error: OpenIddictConstants.Errors.InvalidGrant,
description: context.Request.IsAuthorizationCodeGrantType() ?
"The specified authorization code is no longer valid." :
"The specified refresh token is no longer valid.");
return;
}
}
The context.Reject here comes from the assembly AspNet.Security.OpenIdConnect.Server.
For more details, see source code on GitHub .
I've tried to use a custom middleware to catch all status codes (which it does), but the result is returned before the execution of my custom middleware has completed.
I've tried and I'm pretty sure we can use a custom middleware to catch all status codes. The key point is to detect the status code after the next() invocation:
app.Use(async(context , next )=>{
// passby all other end points
if(! context.Request.Path.StartsWithSegments("/connect/token")){
await next();
return;
}
// since we might want to detect the Response.Body, I add some stream here .
// if you only want to detect the status code , there's no need to use these streams
Stream originalStream = context.Response.Body;
var hijackedStream = new MemoryStream();
context.Response.Body = hijackedStream;
hijackedStream.Seek(0,SeekOrigin.Begin);
await next();
// if status code not 400 , pass by
if(context.Response.StatusCode != 400){
await CopyStreamToResponseBody(context,hijackedStream,originalStream);
return;
}
// read and custom the stream
hijackedStream.Seek(0,SeekOrigin.Begin);
using (StreamReader sr = new StreamReader(hijackedStream))
{
var raw= sr.ReadToEnd();
if(raw.Contains("The specified refresh token is no longer valid.")){
// custom your own response
context.Response.StatusCode = 401;
// ...
//context.Response.Body = ... /
}else{
await CopyStreamToResponseBody(context,hijackedStream,originalStream);
}
}
});
// helper to make the copy easy
private async Task CopyStreamToResponseBody(HttpContext context,Stream newStream, Stream originalStream){
newStream.Seek(0,SeekOrigin.Begin);
await newStream.CopyToAsync(originalStream);
context.Response.ContentLength =originalStream.Length;
context.Response.Body = originalStream;
}