I have created a REST API call in my Angular app which downloads a file.
I am setting responseType to 'blob' since I am expecting a file in response.
But when there is no file available at the server the Response has a error code as 404 i.e Bad Request with some message in body.
But I am not able to parse that error message from body since HttpErrorResponse is giving a blob object in error.error
How do I get the actual body from the error object instead of blob.
Also is there any way to configure angular that on success of an api call parse the request in blob otherwise parse it in json ???
Hoping for a resolution
Try this
if(error.error instanceof Blob) {
error.error.text().then(text => {
let error_msg = (JSON.parse(text).message);
console.log(error_msg)
});
} else {
//handle regular json error - useful if you are offline
}
Parameter: { observe: 'response' }, let you read the full response including the headers. See the below description:-
Tell HttpClient that you want the full response with the observe option:
getConfigResponse(): Observable<HttpResponse<Config>> {
return this.http.get<Config>(this.configUrl, { observe: 'response' });
}
Now HttpClient.get() returns an Observable of typed HttpResponse rather than just the JSON data.
this.configService.getConfigResponse()
// resp is of type `HttpResponse<Config>`
.subscribe(resp => {
// display its headers
const keys = resp.headers.keys();
this.headers = keys.map(key =>
`${key}: ${resp.headers.get(key)}`);
// access the body directly, which is typed as `Config`.
this.config = { ...resp.body };
});
and getting Error body like that:-
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 {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
// return an observable with a user-facing error message
return throwError(
'Something bad happened; please try again later.');
};
import { catchError} from 'rxjs/operators';
getConfig() {
return this.http.get<Config>(this.configUrl)
.pipe(
catchError(this.handleError)
);
}
Reference: https://angular.io/guide/http : Reading the full response
Change your code accordingly.
For future visitors (since the title is generic):
If the backend returns JSON upon error (ideally, following RFC 7807, which would also mean application/problem+json content-type too), the error.error is a JSON object, not a string. So to print it, for example, you would need to stringify it first:
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${JSON.stringify(error.error)}`);
I believe the confusion starts from the official Angular documentation, which contains this statement:
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
But with error.error being a JSON object (in the standard cases), you get printed [object Object] for the body instead of the string representation of that JSON object. Same unhelpful output if you try ${error.error.toString()}.
If the returned ContentType are different then you can leverage it to distinguish whether it's a correct binary file or a text in binary format.
lets consider you have two files, a service, which handles your request and a component which does the business logic
Inside your service, have your download method like:
public downloadFile(yourParams): Observable<yourType | Blob> {
return this._http.post(yourRequestURL, yourParams.body, {responseType: 'blob'}).pipe(
switchMap((data: Blob) => {
if (data.type == <ResponseType> 'application/octet-stream') {
// this is a correct binary data, great return as it is
return of(data);
} else {
// this is some error message, returned as a blob
let reader = new FileReader();
reader.readAsBinaryString(data); // read that message
return fromEvent(reader, 'loadend').pipe(
map(() => {
return JSON.parse(reader.result); // parse it as it's a text.
// considering you are handling JSON data in your app, if not then return as it is
})
);
}
})
);
}
In your component
public downloadFile(params): void {
this._service.downloadFile(params)
subscribe((data: yourType | Blob) => {
if (data instanceof Blob) {
fileSaverSave(data, filename); // use fileSaver or however you are downloading the content
// add an import for saveAs (import { saveAs as fileSaverSave } from 'file-saver';)
} else {
// do your componnet logic to show the errors
}
})
}
If you wish, you can have everything inside your component itself.
You could try a separate error handler function, which returns the response as T as follows -
public handleError<T>(operation = 'operation', result?: T) {
return (error: any): Observable<T> => {
// TODO: send the error to remote logging infrastructure
console.error(error); // log to console instead
// TODO: better job of transforming error for user consumption
console.log(`${operation} failed: ${error.message}`);
// Let the app keep running by returning an empty result.
return of(result as T);
};
}
Then simply use it to track errors in your request as follows -
return this.http.post(this.appconstants.downloadUrl, data, { responseType: 'blob' }).pipe(
map(this.loggerService.extractFiles),
catchError(this.loggerService.handleError<any>('downloadFile')) // <----
);
FYI, the function extractFiles that I used above to return a file is as follows -
public extractFiles(res: Blob): Blob{
return res;
}
Related
I am writing a simple wrapper around fetch.
async function apiCall(
endpoint: string,
{
data,
headers: customHeaders,
...customConfig
}: { data?: Object; headers?: Object } = {}
) {
const config = {
method: data ? 'POST' : 'GET',
body: data ? JSON.stringify(data) : undefined,
headers: {
'content-type': data ? 'application/json' : undefined,
...customHeaders,
},
...customConfig,
}
return fetch(endpoint, config as any).then(async (response) => {
if (response.ok) {
const json = await response.json() // 🤔
return json
} else {
// 👇 🚨 what if `response` contains error messages in json format?
return Promise.reject(new Error('Unknown Error'))
}
})
}
it works fine. The problem is with this snippet
return fetch(endpoint, config as any).then(async (response) => {
if (response.ok) {
const json = await response.json()
return json
} else {
// 👇 🚨 what if `response` contains error messages in json format?
return Promise.reject(new Error('Unknown Error'))
}
})
if response is not ok, it rejects with a generic Error. This is because by default, window.fetch will only reject a promise if the actual network request failed. But the issue is that, even if response is not ok, it might still be able to have error messages in json format. This depends on the backend implementation details but sometimes you are able to get the error messages in the response body by response.json(). Now this use case is not covered in the wrapper I built.
So I wonder how I am going to be able to account for that? I guess you can do something like
fetch(endpoint, config as any).then(async (response) => {
if (response.ok) {
const json = await response.json()
return json
} else {
try {
const json = await response.json()
return Promise.reject(json)
} catch {
return Promise.reject(new Error('Unknown Error'))
}
}
})
but I wonder if there is some more elegant way to do that?
Lastly, I am very aware of libraries like Axios. I built this partly to satisfy my intellectual curiosity.
Btw, a lightly unrelated question but I wonder if these two are equivalent
if (response.ok) {
const json = await response.json()
return json
}
if (response.ok) {
return response.json()
}
Someone flagged my question as a duplicate of this question. In fact they are not the same. I did not make the same assumption as that question did about the API call returning JSON data both on success and on failure. My question is about exactly how we should do in cases where we cannot make such an assumption.
That the response is not ok doesn't prevent you from consuming its body as JSON so your last snippet is indeed how it should be handled.
Now you ask for something "more elegant", well it may not be more elegant but a less redundant way to write the same would be:
fetch(endpoint, config as any).then(async (response) => {
if (response.ok) {
return response.json(); // there is no need to await the return value
}
else {
const err_message = await response.json() // either we have a message
.catch( () => new Error( "Unknown Error" ) ); // or we make one
return Promise.reject( err_message );
}
})
And regarding the last question, yes both are equivalent, the await version doing one more round-trip through the microtask-queue and making your code a bit longer, but for all that matters we could say they are the same.
I'd simplify Kaiido even further, fix some bugs, and add a custom error handler.
class ApiCallError extends Error {
response: Response;
body: any;
httpStatus: number;
constructor(response, body) {
super('HTTP error: ' + response.status);
this.httpStatus = response.status;
this.response = response;
this.body = body;
}
}
async function apiCall(endpoint: string, options?: any) {
const config = {
// do your magic here
}
const response = await fetch(endpoint, config);
if (!response.ok) {
throw new ApiCallError(response, await response.json());
}
return response.json();
}
Changes:
You don't need to catch errors if you're just going to throw again.
You don't need .then() if you support await, and this will make your code a lot simpler.
There's really no point in Promise.resolve and Promise.reject, you can just return or throw.
You should not return plain errors, you should throw them (or make sure they are wrapped in a rejecting promise)
Even though in javascript you can 'throw anything' including strings and arbitrary objects, it's better to throw something that is or extends Error, because this will give you a stack trace.
Making all the error information available on the custom error class provides everything a user of apiCall could possibly need.
I'm trying to implement validation on my Node.js back-end so whenever the data doesn't pass the validation, I'm sending this to the front-end:
if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() });
}
so that I could render the errors on the front-end. Sadly, when I open the console, I only see:
POST http://localhost:3000/login 400 (Bad Request)
as opposed to an object that would contain config, data, headers, request and status. So I am wondering how the hell am I supposed to access the errors object I'm returning to the front-end. I'm following express-validator's docs and this is how they do it as well - https://express-validator.github.io/docs/index.html
You just need to retrieve and parse the response body, even for non-successful requests.
Here's an example using fetch but the approach would be similar using other libs
const doFetch = async (url) => {
const res = await fetch(url, {
// method, headers, body, etc
})
if (!res.ok) {
if (res.status === 400) {
throw await res.json() // this will parse the JSON response body
}
// handle other errors
throw { errors: [ res.statusText ] } // conform to a standard format
}
// handle success
}
doFetch('http://example.com').catch(({ errors }) => {
console.error(errors)
})
I'm creating an api test framework for a project I am working on and I'm looking to validate required fields in JSON objects being sent to an endpoint.
I'm trying to send a JSON object with a missing field and expecting a 400 response from my application with a validation message. But when making this call with axios it (somewhat rightly) throws an error as it received the 400.
I want to be able to assert that 400 is expected and to assert on the validation message.
All the examples I've come across are all regarding dealing with the 400 response in the correct way you would if you are not expecting the 400 response, but i am expecting it.
I couldn't seem to find anyone else trying this.
async function createReminder(reminderObject) {
const response = await axios.post(`${config.get('baseUrl')}/reminder`, {
...reminderObject
});
return response;
}
module.exports.createReminder = createReminder;
Here is my working code as it stands. This will make a valid 200 when being used with a valid call.
I need to have the 400 response / validation message be returned in teh response object to be handled in the function that calls this function.
In effect, you want to intercept the response and transform it as you wish. There's a section in the axios docs dedicated to interceptors. As an example of what you can do with it, here is what I've used in a recent project with a Laravel back-end:
axios.interceptors.response.use(
res => Promise.resolve(res.data),
error => {
let message = null;
if (error.response) {
// if error has a data.error property, it's an error formatted by server
if (error.response.data.error) message = error.response.data.error;
else if (error.response.status === 500) message = 'Oops! Something went wrong.';
} else {
// if error has a code property, it's an error defined by axios
if (error.code === 'ECONNABORTED') message = 'Timeout exceeded';
else message = 'Oops! Something went wrong.';
}
// eslint-disable-next-line prefer-promise-reject-errors
return Promise.reject({ error: message });
}
);
The code above allows me to make axios calls as follows, and always expect the same format:
axios.post('/url', { data: { ... }})
.then(({ jsonProp1 }) => console.log(jsonProp1))
.catch(({ error }) => console.log(error);
Note: if you do create a new axios instance, you need to reinject the response interceptor for this instance too.
It seems my client is not capturing the response value from the server and displaying it.
Here is my component code:
export class MyComponent implements OnInit {
data: string;
constructor(private myService: MyService) {}
ngOnInit() {}
testCall() {
this.myService.getData().subscribe(data => this.data = data);
console.log("Data: " + this.data);
}
}
The service code:
#Injectable()
export class MyService {
private url = 'http://localhost:5000/myproj/api/test';
constructor(private http: HttpClient) { }
// Get data from the server
getData(): Observable<string> {
console.log("in getData() method");
return this.http.get<string>(this.url)
.pipe(
catchError(this.handleError) // then handle the error
);
}
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 {
// The backend returned an unsuccessful response code.
// The response body may contain clues as to what went wrong,
console.error(
`Backend returned code ${error.status}, ` +
`body was: ${error.error}`);
}
// return an observable with a user-facing error message
return new ErrorObservable('Something went wrong; please try again later.');
};
}
The request goes to the server, and the server responds with the data in the response body, and a status code of 200, which you can see in developer tools in Internet Explorer:
But for some reason, when I call the service method getData(), the angular client code calls the catchError() method I defined, and prints:
Backend returned code 200, body was: [object Object]
ERROR Something went wrong; please try again later.
Why is the server returning status 200 (OK), but the Angular client is calling the catchError() method?
EDIT:
Here is my server side API code:
#RequestMapping(value = "/test", method = RequestMethod.GET, produces = "text/plain")
public String testApi(HttpServletRequest request) {
System.out.println("in /test");
String response = "my response";
return response;
}
The Response Body is not proper JSON format, hence the "Invalid character" error which is produced from the deserialization. The service is expecting properly formed JSON.
Update your API to return a valid JSON object by using "application/json" and returning an object as shown in the following post: Spring MVC - How to return simple String as JSON in Rest Controller
You need to place the console.log inside the .subscribe() method
this.myService.getData().subscribe(data => {
this.data = data;
console.log(this.data);
});
You have to set responseType to 'text' in a request options object. Here's a sample:
return this.http.get(`myApi/ExampleMethod/param`, { responseType: 'text' })
.pipe(
catchError(
this.errorHandler.handleError.bind(this)
)
);
I've written a function to send a http put request to update some data but it says, that it is not recieving any data:
updateHuman(human: Human) {
const url = `${this.url}/${human.id}`;
const data = JSON.stringify(human);
return this.http.put(url, data).map(
response => response.json().data as Human,
error => console.log(error)
);
}
After I've changed my function to the following, it is working:
updateHuman(human: Human) {
const url = `${this.url}/${human.id}`;
const data = JSON.stringify(human);
return this.http.put(url, data).map(() => human);
}
Could someone explain me, why the first function is not working but second is working?
Observables are lazy, you need to be subscribed to them for them to work and retrieve anything. Did you subscribe to your method? Example:
methodToUpdateHuman(human): void{
...
this.updateHuman(human).subscribe((response) => {
//do something with the response
console.log.("Response is: ", response);
},
(error) => {
//catch the error
console.error("An error occurred, ", error);
});
}
I suggest you read through the Angular Tour Of Heroses, it's based in angular 2 and most of the functionality is functional in angular 4, there is a section dedicated to http requests: https://angular.io/tutorial/toh-pt6
In the second example you are not returning the response within the map, you are returning the human that was originally passed in.
So, basically you are creating an illusion that it is working, when it isn't.
Probably best to test your API with something like PostMan, to see if you can get it working with that first.
You use map method incorrectly, read more about this method in documentation: http://xgrommx.github.io/rx-book/content/observable/observable_instance_methods/map.html
If you want receive response from server your code should look like that:
updateHuman(human: Human) {
const url = `${this.url}/${human.id}`;
const data = JSON.stringify(human);
return this.http.put(url, data).subscribe(
response => response.json().data as Human,
error => console.log(error)
);
}
You can use map method if you want to modify server response(map some objects to other structures etc.):
updateHuman(human: Human) {
const url = `${this.url}/${human.id}`;
const data = JSON.stringify(human);
return this.http.put(url, data)
.map(response => { return response.json() }) // you can get json response here
.subscribe(
response => response.data as Human, // -- change here --
error => console.log(error)
);
}
map method returns Observable object, so you can subscribe that and wait for response, error or simple complete method(third parameter of subscribe()):
http://xgrommx.github.io/rx-book/content/observable/observable_instance_methods/subscribe.html