I am developing an Angular application that shows some images to the user.
I would like to obtain those images from a single REST call to a web service: given the fact i am already uploading the images via a FormData object, i would like to receive those images in the same way (so, basically, via content-type: multipart/form-data).
At the moment, using the following code:
this.http.post('api/load', {}, {headers: {'Accept': 'multipart/form-data'},
responseType:'text', observe: 'response'});
i am actually receiving the full body of the response in a text format, like this:
--974b5730-ab25-4554-8a69-444664cab379
Content-Disposition: form-data; name=result
{"bar":[1,2,3,4], "foo": true}
--974b5730-ab25-4554-8a69-444664cab379
Content-Disposition: form-data; name=image; filename=image1.jpg; filename*=utf-8''image1.jpg
--- binarycontent...
But it's in a raw, text format.
How can i receive a multipart/form-data response formatted by its boundaries, or a in clean way in Angular 7.x?
One of the solution is to implement an interceptor service where you can format multipart/form-data response.
For example, your inteceptor will be - multipart.interceptor.ts :
#Injectable()
export class MultipartInterceptService implements HttpInterceptor {
private parseResponse(response: HttpResponse<any>): HttpResponse<any> {
const headerValue = response.headers.get('Content-Type');
const body = response.body;
const contentTypeArray = headerValue ? headerValue.split(';') : [];
const contentType = contentTypeArray[0];
switch (contentType) {
case 'multipart/form-data':
if (!body) {
return response.clone({ body: {} });
}
const boundary = body?.split('--')[1].split('\r')[0];
const parsed = this.parseData(body, boundary); // function which parse your data depends on its content (image, svg, pdf, json)
if (parsed === false) {
throw Error('Unable to parse multipart response');
}
return response.clone({ body: parsed });
default:
return response;
}
}
// intercept request and add parse custom response
public intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(customRequest).pipe(
map((response: HttpResponse<any>) => {
if (response instanceof HttpResponse) {
return this.parseResponse(response);
}
})
);
}
}
Related
I just upgraded my react app to axios 0.25.0 and started getting http 415 Unsupported Media type errors in some REST POST/PUT API calls. These calls all have one thing in common, they include a date field in the request body.
I use an axios transformRequest to ensure that any date fields are sent to the API in local time rather than the JSON default UTC.
When I look at the header for these requests the "Content-Type" has changed from "application/json" to "application/x-www-form-urlencoded" and is subsequently rejected by the REST API which expects [FromBody] JSON object.
Rather than change the API I have added an axios interceptor to ensure the header "Content-Type" is "application/json". This fixes the problem but I do not like the fix.
transformRequest
const serialiseDateLocal = (data: any) => {
Date.prototype.toJSON = function() {
// dateToString returns local time in specified format
return dateToString(this, "yyyy-MM-ddTHH:mm:ss.sssZ");
};
return JSON.stringify(data);
}
axios.defaults.transformRequest = [serialiseDateLocal];
interceptor
axios.interceptors.request.use(req => {
if(req.headers) {
req.headers["Content-Type"] = 'application/json';
}
return req;
});
Why is the content-type changing to "application/x-www-form-urlencoded" when the transform is used? is there a way to prevent this or a better way to handle it?
UPDATE:
Setting default headers negates the need for the interceptor.
axios.defaults.headers.common["Content-Type"] = "application/json";
axios.defaults.headers.post["Content-Type"] = "application/json";
axios.defaults.headers.put["Content-Type"] = "application/json";
axios.defaults.headers.patch["Content-Type"] = "application/json";
UPDATE:
Based on Phils comments problem turned out to be me replacing the transformRequest chain as well as returning a string from my transformer - solution is.
const serialiseDateLocal: AxiosRequestTransformer = (data: any) => {
if (data instanceof Date) {
return dateToString(data, serializedFormat);
}
if (typeof data === "object" && data !== null) {
return Object.fromEntries(Object.entries(data).map(([ key, val ]) =>
[ key, serialiseDateLocal(val) ]))
}
return data;
}
axios.defaults.transformRequest = [serialiseDateLocal].concat(axios.defaults.transformRequest);
The default content-type for Axios requests is application/x-www-form-urlencoded.
One thing the default request transformer does is set the appropriate content-type for the request data...
if (utils.isObject(data) || (headers && headers['Content-Type'] === 'application/json')) {
setContentTypeIfUnset(headers, 'application/json');
return stringifySafely(data);
}
Since you're replacing the entire transformRequest chain, you now miss out on this.
You should either handle it in your transformer, eg
import { AxiosRequestTransformer } from "axios"
const serialiseDateLocal: AxiosRequestTransformer = (data, headers) => {
// TODO: actually transform the request, don't just modify the Date prototype
headers["Content-Type"] = "application/json"
return JSON.stringify(data)
}
or as is more common, unshift your transformer onto the existing chain and have it return a plain object, letting the default transformer do its work
const instance = axios.create({
transformRequest: [ serialiseDateLocal ].concat(axios.defaults.transformRequest)
})
I'm not sure yet why your interceptor wasn't setting the header correctly. I suspect it's a race condition and it might be worth raising a bug report.
I am working to download a pdf file from User Interface (Angular), generated from Spring boot .
I am able to download pdf file from browser with same API.
Quick help will be much appreciated.
In postman it gives response like this -
When trying from UI then getting below error-
SyntaxError: Unexpected token % in JSON at position 0 at JSON.parse () at XMLHttpRequest.onLoad
message: "Unexpected token % in JSON at position 0"
stack: "SyntaxError: Unexpected token % in JSON at position 0↵ at JSON.parse ()↵ at XMLHttpRequest.onLoad (http://localhost:4200/vendor.js:19662:51)↵
API Code
Controller code-
#RequestMapping(value = "/downloadPDF/", method = RequestMethod.GET, produces = "application/pdf")
public ResponseEntity<Resource> downloadServicePack(#RequestHeader("Authorization") String token,
HttpServletRequest request) throws WorkflowException, Exception {
String fileName = "TWEVL_ServiceDesignPack.pdf";
// String fileName ="ServiceDesignPdf.pdf";
Resource resource = fileStorageService.loadFileAsResource(fileName);
// Try to determine file's content type
String contentType = null;
try {
contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath());
} catch (IOException ex) {
// logger.info("Could not determine file type.");
}
// Fallback to the default content type if type could not be determined
if (contentType == null) {
contentType = "application/octet-stream";
}
return ResponseEntity.ok().contentType(MediaType.parseMediaType(contentType))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resource.getFilename() + "\"")
.body(resource);
}
Service Code-
#Service
public class FileStorageService {
private final Path fileStorageLocation;
#Autowired
public FileStorageService(FileStorageProperties fileStorageProperties) throws Exception {
this.fileStorageLocation = Paths.get(fileStorageProperties.getUploadDir())
.toAbsolutePath().normalize();
try {
Files.createDirectories(this.fileStorageLocation);
} catch (Exception ex) {
throw new Exception("Could not create the directory where the uploaded files will be stored.", ex);
}
}
public Resource loadFileAsResource(String fileName) throws Exception {
try {
Path filePath = this.fileStorageLocation.resolve(fileName).normalize();
Resource resource = new UrlResource(filePath.toUri());
if(resource.exists()) {
return resource;
} else {
throw new Exception("File not found " + fileName);
}
} catch (Exception ex) {
throw new Exception("File not found " + fileName, ex);
}
}
}
Angular Code-
JWT Interceptor to pass token & other header-
#Injectable()
export class JwtInterceptor implements HttpInterceptor {
constructor(private authenticationService: AuthenticationService) {}
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
// add authorization header with jwt token if available
const token = this.authenticationService.currentUserValue;
request = request.clone({
setHeaders: {
Authorization: `Bearer ${token.token}`,
Accept: `application/pdf`,
responseType:'blob',
'Content-Type':`application/pdf`
}
});
return next.handle(request);
}
}
API Call
downloadServicePackPDF() {
this.tcmtSrv
.downloadServicePack()
.subscribe(
(blob: Blob) => {
console.log('report is downloaded');
},
(error) => {
console.log(error);
}
);
}
Service Code -
downloadServicePack() {
//header is being passed from interceptor
return this.apiSrv.get(DOWNLOAD_SERVICE_PACK,'');
}
Request Header-
I was not passing responseType in the interceptor. I tried to pass it service ts but Interceptor was overriding these headers & other params given at service level.
Passed header like below in interceptor & it worked -
const newRequest = request.clone({ setHeaders: { Authorization: Bearer ${token.token}, "Content-Type": "text/plain", Accept: "text/plain" },responseType: "text", });
I work with a Backend API which returns different data types for different requests to the same endpoint. While a more appropriate solution would be to unify the data type returned, legacy, time and lack of tests play against this solution.
I am centralizing my call method to be used by other parts of the application which need to call the endpoint. This call method implements fetch. For information:
export default function call<P> (method: TCallMethod, payload: P, parameter?: string): Promise<IServerResponseObject> {
const url: string = buildUrl(parameter);
const body: string | null = payload ? JSON.stringify(payload) : null;
return fetch(url, {
method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${getAuthToken()}`
},
body
}).then(async (response) => {
let body: IServerResponseObjectBody = {
message: '',
code: ''
};
if (response) {
body = await response.json();
}
return {
status: response.status,
body
};
});
}
As I receive data, I am using the Response.json method to decode it.
if (response) {
body = await response.json();
}
The problem is that sometimes I receive no data (when the user is not authenticated - although that's an edge case) or the server responds with just a boolean.
In that case, the json() execution fails, because we are not handling JSON data.
ie:
FetchError: invalid json response body at http://localhost:4545/api/definition/isNameUnique/used%20name reason: Unexpected end of JSON input
I am wondering if there is a cleaner way than nesting try/catches to determine which decode method to use from the ones available: https://developer.mozilla.org/en-US/docs/Web/API/Body#Methods
This feels like a potential solution: https://developer.mozilla.org/en-US/docs/Web/API/Body#Properties but the documentation is not too explicit and lacks examples on how to use it.
It sounds to me like you want to use text to read the response, then look at the resulting text and decide what to do. Roughly:
const text = await response.text();
if (!text) {
// no response, act accordingly
} else if (reBool.test(text)) {
// boolean response, determine whether it's true or false and act on it
} else {
// JSON response, parse it
data = JSON.parse(text);
// ...then use it
}
...where reBool is a regular expression to test for the boolean the server sometimes returns, for instance /^(?:true|false)$/i.
If the response may have whitespace, you might trim the result of response.text().
There are some unrelated things you might also want to do:
You're not checking for a successful response (this is a mistake a lot of people make, so many I wrote it up on my otherwise-anemic little blog). Check response.ok before using json or text, etc.
It doesn't make much sense to pass an async function into then as a callback. If you're going to go async, do it earlier, by making call an async function, then work with await throughout the body rather than mixing your metaphors...
Addressing those and folding in the main answer above (you'll need to adjust as necessary, either IServerResponseObject needs changing or you need to do something different with boolean responses):
const reBool = /^(?:true|false)$/i;
export default async function call<P> (method: TCallMethod, payload: P, parameter?: string): Promise<IServerResponseObject> {
const url: string = buildUrl(parameter);
const body: string | null = payload ? JSON.stringify(payload) : null;
const response = await fetch(url, {
method,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Bearer ${getAuthToken()}`
},
body
});
const {status} = response;
if (!response.ok) {
throw new Error("HTTP error " + status); // Or `return {status};` or similar, but making it an error is useful
}
const text = (await response.text()).trim();
let result = {status};
if (!text) {
// blank, act accordingly, perhaps:
result.body = null;
} else if (reBool.test(text)) {
result.body = text === "true";
} else {
result.body = JSON.parse(text);
}
return result;
}
Receiving HTTP Failure during parsing in Angular. Goal is to download a csv file from the api response
Controller:
downloadFile(data) {
const blob = new Blob([data], { type: 'text/csv' });
const url = window.URL.createObjectURL(blob);
window.open(url);
}
getFileDownload(): void {
this.iportalUploadService.getFileDownload(this.fileName).subscribe(data => {
debugger;
this.fileDownload = data;
this.downloadFile(data);
});
}
Service:
private fileDownloadUrl = 'file-transfer/validationErrorsCSV';
formHtppOptions(params): any {
const httpOptions = {
headers: { 'Application-Token': this.getToken() },
params: params,
};
return httpOptions;
}
getFileDownload(fileName): Observable < Object > {
const baseUrl = this.getBaseUrl();
return this.http.get<Object>(baseUrl + this.fileDownloadUrl, this.formHtppOptions({ fileName: fileName }));
}
Below is the console error I am receiving
console error
Response format Photo
Response photo
You are getting this error because your response is not in JSON format. You are trying to convert it into an object and CSV text cannot be parsed to a proper json object. Here is what you might want to do:
getFileDownload(fileName): Observable<any> {
const baseUrl = this.getBaseUrl();
return this.http.get(baseUrl + this.fileDownloadUrl, this.formHtppOptions({fileName: fileName})).pipe(map((data:any) => this.converter.ToJson(data)));
}
Usually, I have a "converter" service that does this kind of parsing. You can make use of papa parse, or parse yourself by looping through the response.
Update: Here is an example of manually parsing the response: http://blog.sodhanalibrary.com/2016/10/read-csv-data-using-angular-2.html
Have a look at the above blog post.
I resolved this issue by adding responseType: 'text' in formhttpOtions.
I'm sending a request to an API, it returns an array of data, but I don't know how to extract the headers from that url, this is what i've tried in my service
#Injectable()
export class ResourcesService {
private resourcesurl = "http://localhost:9111/v1/resources";
constructor(private http: Http) { }
getResources() {
let headers = new Headers();
headers.append("api_key", "123456");
return this.http.get(this.resourcesurl, { headers: headers
}).map(this.extractData).catch(this.handleError);
}
getresourceheaders(){
let headers = new Headers();
headers.append("api_key", "123456");
let options = new RequestOptions();
let testsss = options.headers
let headerapi = this.http.request(this.resourcesurl, options);
let test = this.http.get(this.resourcesurl, { headers: headers });
console.log(headerapi);
}
private extractData(res: Response) {
let body = res.json();
return body.data || {};
}
private handleError(error: Response | any) {
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
I want to get the headers from that response that in this case is resourceurl
any idea?
Clear angular 5 answer
By default, this.http.whatever's returned observable will be on the data returned, not the HttpResponse.
If you have a peak at: https://angular.io/api/common/http/HttpClient
You'll notice the options take an "observe" parameter of a HttpObserve type. While it's not documented what the HttpObserve is, if you put it as "response" then you will instead receive an instance of HttpResponse<T>(https://angular.io/api/common/http/HttpResponse)
So, here's an example request:
this.http.get(url, {observe: 'response'})
.subscribe(resp => console.log(resp.headers))
Note: Due to browser cors security, you will not be-able to see headers unless the API provides Access-Control-Expose-Headers: with your custom headers if your api and angular app do not have the same domain.
The headers are part of the Response class, so you should be able to see them in a handler like
http.get('/path/to/resource')
.subscribe((res:Response) => {
console.log(res.headers);
// you can assign the value to any variable here
});
When you do .map(this.extractData) the let body = res.json() from this.extractData function takes out everything from the response except the body.
Instead if you do following, .map((res: Response) => res), that will return the whole response and you can access all the attributes and assign them to variables.
Here's a Plunker demo.
A bit more of an exotic example in Angular 5 shown below. Using HttpClient to post to a GraphQL server, read the response and then extract a response header value and a response body value. The header is Total-Count in this case. cars is a field (array of Car) under another field data in the body. Also shows use of the rxjs first operator.
import { HttpClient, HttpHeaders, HttpResponse } from '#angular/common/http';
import { first } from 'rxjs/operators/first';
import { Car, CarPage } from '../models/car';
..........
..........
public find(filter: string, sort: string, limit: number): Observable<CarPage> {
let headers = new HttpHeaders().set("Content-Type", "application/graphql");
let carPage: CarPage = { cars: [], totalCount: 0 };
return this.http.post<HttpResponse<any>>('/graphql',
`query cars { cars(filter: "${filter}", sort: "${sort}", limit: ${limit}) {
id
make
model
year
}
}`,
{ headers: headers, observe: "response" }
)
.first((_, index) => index === 0, (response: HttpResponse<any>) => {
let totalCountHeaderValues = response.headers.getAll("Total-Count");
carPage.totalCount = (totalCountHeaderValues.length > 0) ? parseInt(totalCountHeaderValues[0]) : 0;
carPage.cars = response.body.data.cars;
return carPage;
})
}
The return type of the angular Http.get method returns a Response type. This object has a headers object that contains information about the headers. It also has a url property.
this.http.get(url).map(resp => console.log(resp));