How to customize the status code of the Spring Cloud Gateway custom exception based on the exception - spring-cloud

I found that I could not get the status code of the exception when customizing the exception in the gateway, so I don’t know how to set the custom status code instead of setting all the exception status codes to the same.

Spring Cloud Gateway custom handlers exception handler. Example for version Spring Cloud 2020.0.3 . Common methods have your own defaultErrorWebexceptionHandler or only ErrorAttributes.
Method 1: ErrorWebexceptionHandler (for schematic only). Customize a globalrrorattributes:
#Component
public class GlobalErrorAttributes extends DefaultErrorAttributes{
#Override
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
Throwable error = super.getError(request);
Map<String, Object> map = super.getErrorAttributes(request, options);
map.put("status", HttpStatus.BAD_REQUEST.value());
map.put("message", error.getMessage());
return map;
}
}
#Component
#Order(-2)
public class GlobalErrorWebExceptionHandler extends AbstractErrorWebExceptionHandler {
public GlobalErrorWebExceptionHandler(GlobalErrorAttributes gea, ApplicationContext applicationContext,
ServerCodecConfigurer serverCodecConfigurer) {
super(gea, new WebProperties.Resources(), applicationContext);
super.setMessageWriters(serverCodecConfigurer.getWriters());
super.setMessageReaders(serverCodecConfigurer.getReaders());
}
// Rendering HTML or JSON.
#Override
protected RouterFunction<ServerResponse> getRoutingFunction(final ErrorAttributes errorAttributes) {
return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
}
private Mono<ServerResponse> renderErrorResponse(final ServerRequest request) {
final Map<String, Object> errorPropertiesMap = getErrorAttributes(request, ErrorAttributeOptions.defaults());
return ServerResponse.status(HttpStatus.BAD_REQUEST)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(errorPropertiesMap));
}
}
Reference document: https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/autoconfigure/web/reactive/error/AbstractErrorWebExceptionHandler.html
Method 2: Only one ErroraTributes to overwrite the default defaultrRorattributes .
#Component
public class GatewayErrorAttributes extends DefaultErrorAttributes {
private static final Logger logger = LoggerFactory.getLogger(GatewayErrorAttributes.class);
#Override
public Map<String, Object> getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
Throwable error = super.getError(request);
Map<String, Object> errorAttributes = new HashMap<>(8);
errorAttributes.put("message", error.getMessage());
errorAttributes.put("method", request.methodName());
errorAttributes.put("path", request.path());
MergedAnnotation<ResponseStatus> responseStatusAnnotation = MergedAnnotations
.from(error.getClass(), MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(ResponseStatus.class);
HttpStatus errorStatus = determineHttpStatus(error, responseStatusAnnotation);
// Must set, otherwise an error will be reported because the RendereRRRRESPONSE method of DefaultErrorWebexceptionHandler gets this property, re-implementing defaultErrorWebexceptionHandler.
errorAttributes.put("status", errorStatus.value());
errorAttributes.put("code", errorStatus.value());
// html view
errorAttributes.put("timestamp", new Date());
// html view
errorAttributes.put("requestId", request.exchange().getRequest().getId());
errorAttributes.put("error", errorStatus.getReasonPhrase());
errorAttributes.put("exception", error.getClass().getName());
return errorAttributes;
}
// Copy from DefaultErrorWebexceptionHandler
private HttpStatus determineHttpStatus(Throwable error, MergedAnnotation<ResponseStatus> responseStatusAnnotation) {
if (error instanceof ResponseStatusException) {
return ((ResponseStatusException) error).getStatus();
}
return responseStatusAnnotation.getValue("code", HttpStatus.class).orElse(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
Attention please: ErroraTributes.put ("status", errorstatus.value ()), otherwise an error is reported because the rendererrorResponse method of DefaultErrorwebexceptionHandler gets this property. Unless you re-implement the defaultErrorWebexceptionhandler like yourself.
Then visit an unduly service in the gateway to see the effect.
curl 'http://127.0.0.1:8900/fundmain22/abc/gogogo?id=1000' --header 'Accept: application/json'
{"exception":"org.springframework.web.server.ResponseStatusException","path":"/fundmain22/abc/gogogo","code":404,"method":"GET","requestId":"094e53e5-1","message":"404 NOT_FOUND","error":"Not Found","status":404,"timestamp":"2021-08-09T11:07:44.106+0000"}

Related

JsonpRequestBuilder with typed response throws InCompatibleClassChangeError

I have an existing app that I'm adding a "Suggested Products" feature to and I'm having trouble with my JSONP response not being properly transformed to the typed JsArray. I'm hoping someone can give me an idea of what I'm doing wrong?
I have defined my type that will be returned from the server in its own class:
import com.google.gwt.core.client.JavaScriptObject;
public class SuggestedProduct extends JavaScriptObject {
protected SuggestedProduct() {}
public final native String getFormName();
public final native String getImageURL();
}
I have a method that uses the JsonpRequestBuilder to fire off a request to get my JSON.
private void loadSuggestedProducts() {
JsonpRequestBuilder builder = new JsonpRequestBuilder();
builder.requestObject(buildSuggestedProductURL(), new AsyncCallback<JsArray<SuggestedProduct>>() {
public void onFailure(Throwable caught) {
//Handle errors
}
public void onSuccess(JsArray<SuggestedProduct> data) {
if ( data == null) {
//Handle empty data
return;
}
SafeHtmlBuilder sb = new SafeHtmlBuilder();
sb.appendHtmlConstant("<h4>Suggested Products:</h4>");
for (int i=0; i < data.length(); i++) {
SuggestedProduct product = data.get(i); //<- This line throws the exception
sb.appendHtmlConstant("<div class=\"card\">");
sb.appendHtmlConstant("<img class=\"card-img-top\" src=\"" + product.getImageURL() + "\" alt=\"" + product.getFormName() + "\">");
sb.appendHtmlConstant("<div class=\"card-body\">");
sb.appendHtmlConstant("<h5 class=\"card-title\">" + product.getFormName() + "</h5>");
sb.appendHtmlConstant("<a onclick=\"javascript:addItems();\" class=\"cmd-add\">Add <i aria-hidden=\"true\" class=\"fa fa-plus-circle\"></i></a>");
sb.appendHtmlConstant("</div></div>");
}
view.getSuggestedProducts().setInnerSafeHtml(sb.toSafeHtml());
}
});
}
When I try to use a SuggestedProduct from the response, I get an error:
java.lang.IncompatibleClassChangeError: Found interface
com.google.gwt.cor.client.JsArray, but class was expected
I've been following the guide in the GWT documentation. I don't see any difference between what I'm trying and what they say will work. When I debug, it looks as though the returned data is an array of SuggestedProducts, so I'm stumped as to how to proceed. Any help would be appreciated.
After closer inspection I realized my overlay type was missing method bodies for what fields to return from the JSON object they represented. The fix was to include the proper JSNI method definitions.
import com.google.gwt.core.client.JavaScriptObject;
public class SuggestedProduct extends JavaScriptObject {
protected SuggestedProduct() {}
public final native String getFormName() /*-{ return this.formname; }-*/;
public final native String getImageURL() /*-{ return this.imageurl; }-*/;
}

How to make the #RestController do not response data as restful? [duplicate]

I have a REST endpoint implemented with Spring MVC #RestController. Sometime, depends on input parameters in my controller I need to send http redirect on client.
Is it possible with Spring MVC #RestController and if so, could you please show an example ?
Add an HttpServletResponse parameter to your Handler Method then call response.sendRedirect("some-url");
Something like:
#RestController
public class FooController {
#RequestMapping("/foo")
void handleFoo(HttpServletResponse response) throws IOException {
response.sendRedirect("some-url");
}
}
To avoid any direct dependency on HttpServletRequest or HttpServletResponse I suggest a "pure Spring" implementation returning a ResponseEntity like this:
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(newUrl));
return new ResponseEntity<>(headers, HttpStatus.MOVED_PERMANENTLY);
If your method always returns a redirect, use ResponseEntity<Void>, otherwise whatever is returned normally as generic type.
Came across this question and was surprised that no-one mentioned RedirectView. I have just tested it, and you can solve this in a clean 100% spring way with:
#RestController
public class FooController {
#RequestMapping("/foo")
public RedirectView handleFoo() {
return new RedirectView("some-url");
}
}
redirect means http code 302, which means Found in springMVC.
Here is an util method, which could be placed in some kind of BaseController:
protected ResponseEntity found(HttpServletResponse response, String url) throws IOException { // 302, found, redirect,
response.sendRedirect(url);
return null;
}
But sometimes might want to return http code 301 instead, which means moved permanently.
In that case, here is the util method:
protected ResponseEntity movedPermanently(HttpServletResponse response, String url) { // 301, moved permanently,
return ResponseEntity.status(HttpStatus.MOVED_PERMANENTLY).header(HttpHeaders.LOCATION, url).build();
}
As the redirections are usually needed in a not-straightforward path, I think throwing an exception and handling it later is my favourite solution.
Using a ControllerAdvice
#ControllerAdvice
public class RestResponseEntityExceptionHandler
extends ResponseEntityExceptionHandler {
#ExceptionHandler(value = {
NotLoggedInException.class
})
protected ResponseEntity<Object> handleNotLoggedIn(
final NotLoggedInException ex, final WebRequest request
) {
final String bodyOfResponse = ex.getMessage();
final HttpHeaders headers = new HttpHeaders();
headers.add("Location", ex.getRedirectUri());
return handleExceptionInternal(
ex, bodyOfResponse,
headers, HttpStatus.FOUND, request
);
}
}
The exception class in my case:
#Getter
public class NotLoggedInException extends RuntimeException {
private static final long serialVersionUID = -4900004519786666447L;
String redirectUri;
public NotLoggedInException(final String message, final String uri) {
super(message);
redirectUri = uri;
}
}
And I trigger it like this:
if (null == remoteUser)
throw new NotLoggedInException("please log in", LOGIN_URL);
if you #RestController returns an String you can use something like this
return "redirect:/other/controller/";
and this kind of redirect is only for GET request, if you want to use other type of request use HttpServletResponse

AEM 6.1 Sightly basic form submit and redirect to same page

I am trying to do the following on AEM 6.1:
Develop a simple form (3 input fields)
Process the submitted values,
And redirect to the same page with processed values/result
I am able to submit the values to a servlet, and process them (business logic), and the result to a requestparamter so i can retrieve them on the UI. But i am stuck at these:
Redirecting to the same page
And retrieving the request parameters and display them using Sightly.
Code Snippets:
Servlet
#SlingServlet(
methods = { "POST","GET" },
name="com.tti.tticommons.service.servlets.LeadTimeTrendsServlet",
paths = { "/services/processFormData" }
)
public class TTICommonServlet extends SlingAllMethodsServlet{
...
#Override
protected void doPost(SlingHttpServletRequest request,SlingHttpServletResponse response) throws ServletException,IOException {
String result;
try {
Enumeration<String> parameterNames = request.getParameterNames();
Map<String, String> formParametersMap = new HashMap<String, String>();
while (parameterNames.hasMoreElements()) {
paramName = parameterNames.nextElement();
paramValue = request.getParameter(paramName);
.......
.......
}
request.setAttribute("result",result);
response.sendRedirect("/content/ttii/en/**posttest.html**");
}
}
Can anyone please help on ho to retireve the above "result" in posttest.html using sightly.
After lot of research and several trials, i finally had the code working. I had to pick up related info from several answers in stackoverflow. Thanks to all the authors. Posting my solution here so beneficial for others.
Result Form with response from webservice:
Process flow
Submit form data to Servlet's POST method
In Servlet, get the values entered by the user from the request
Make the necessary webservice calls. Get the response(json)
I added the response-json as a parameter to the request
Using Wrapper, forward to the necessary page
Define a WCMUse class for use with Sightly.
Assign the 'request' to the Use-class and process it there
Use the assigned values from the Use-class to the UI using sightly
Code snippets - HTML
<form name="userRegistrationForm" method="post" action="/services/processFormData">
<input type="hidden" name=":redirect" value="posttest.html" />
<input type="submit" title="Submit" class="btn submit btn-success" value="Submit" tabindex="25" name="bttnAction">
<div data-sly-use.model="${'com.abccommons.service.helpers.PostServiceHelper' # slingreq=request }">
**${model.getRawJson}**
</div>
Code snippets - Servlet
#SlingServlet(
label = "ABC - Common Servlet",
metatype = true,
methods = { "POST" },
name="com.abccommons.service.servlets.ABCPostServlet",
paths = { "/services/processFormData" }
)
public class ABCPostServlet extends SlingAllMethodsServlet{
#Override
protected void doPost(SlingHttpServletRequest request,SlingHttpServletResponse response) throws ServletException,IOException {
log.info("\n\n----- ABCPostServlet POST: ");
String paramName;
String paramValue;
String osgiService="";
try {
Enumeration<String> parameterNames = request.getParameterNames();
Map<String, String> formParametersMap = new HashMap<String, String>();
while (parameterNames.hasMoreElements()) {
paramName = parameterNames.nextElement();
paramValue = request.getParameter(paramName);
if (paramName.equals("osgiService")) {
osgiService = paramValue;
} else if (paramName.equals(":cq_csrf_token")) {
//TODO: don't add to the map
} else if (paramName.equals("bttnAction")) {
//TODO: dont' add to the map
} else {
//log.info("\n---ParamName="+paramName+", value="+paramValue);
formParametersMap.put(paramName, paramValue);
}
}
String parametersInJSON = JSONHelper.toJson(formParametersMap);
log.info("\n\n----------- POST paramters in json="+parametersInJSON);
String json = webServiceHelper.getJSON(osgiService, parametersInJSON, request, response);
log.info("\n\n----------- POST json from web service="+json);
request.setAttribute("jsonResponse",json);
//String redirectPage = request.getParameter(":redirect");
//RequestDispatcher dispatcher = request.getRequestDispatcher("/content/en/"+redirectPage);
RequestDispatcher dispatcher = request.getRequestDispatcher("/content/en/postformtest.html");
GetRequest getRequest = new GetRequest(request);
dispatcher.forward(getRequest, response);
} catch (Exception e) {
log.error("SlingServlet Failed while retrieving resources");
} finally {
//TODO
}
}
/** Wrapper class to always return GET for AEM to process the request/response as GET.
*/
private static class GetRequest extends SlingHttpServletRequestWrapper {
public GetRequest(SlingHttpServletRequest wrappedRequest) {
super(wrappedRequest);
}
#Override
public String getMethod() {
return "GET";
}
}
Code snippets - PostServiceHelper - WCMUSe class
public class PostServiceHelper extends WCMUse {
protected final Logger log = LoggerFactory.getLogger(PostServiceHelper.class);
private SlingHttpServletRequest httpRequest;
private String rawJson;
#Override
public void activate() throws Exception {
log.info("\n\n========= PostServiceHelper.activate():"+get("slingreq", SlingHttpServletRequest.class));
this.httpRequest = get("slingreq", SlingHttpServletRequest.class);
//this.resourceResolver = getResourceResolver();
//log.info("\n\n========= getRequest()="+getRequest());
SlingHttpServletRequest tRequest;
Set<String> keys = new HashSet<String>();
Enumeration<?> attrNames = this.httpRequest.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attr = (String) attrNames.nextElement();
//log.info("\n--- Key="+attr);
if (attr.equals("jsonResponse")) {
this.setRawJson((String)this.httpRequest.getAttribute(attr));
//log.info("\n---rawJson is SET with : "+this.rawJson);
}
}
}
public void setRawJson(String json) {
this.rawJson = json;
}
public String getRawJson() {
return this.rawJson;
}
}
This is actually a rather tricky pattern to achieve in Sling. You may be better served by submitting the form asynchronously and updating your HTML dynamically via JavaScript.
If you do need to submit your form in the manner you specify, then your servlet needs to produce the HTML response. To produce a response made up of a rendering of the page identified by the requested path your servlet will need to dispatch the request to the appropriate rendering mechanism. You can reference Get JSP output within Servlet in AEM for information concerning how that can be accomplished. Upon dispatch your page and its components should have access to the submitted form values as well as the attributes set on the request.

JERSEY - Accessing Generic List in Response

Im facing isssue in getting Jersey Generic List in client response. I need to get it as Entity for some reason.
#XmlRootElement(name="list")
#XmlSeeAlso({RESTDomain.class})
public class JAXBContainer<T> {
private List<T> items = new ArrayList<T>();
public JAXBContainer() { }
public JAXBContainer(List<T> items) {
this.items = items;
}
#XmlElementWrapper(name="items")
#XmlAnyElement(lax=true)
public List<T> getItems() {
return items;
}
public void setItems(List<T> items) {
this.items = items;
}
#XmlAttribute
public int getItemsSize() {
return this.items.size();
}
above is my Generic List to the resopnse
#GET
#Produces({MediaType.APPLICATION_XML})
public Response getREST(){
RESTDomain domain = new RESTDomain();
domain.setName("Adams");
domain.setPlace("Zurich");
List<RESTDomain> restDomains = new ArrayList<RESTDomain>();
restDomains.add(domain);
JAXBContainer<RESTDomain> jAXBContainer= new JAXBContainer<RESTDomain>(restDomains);
GenericEntity<JAXBContainer<RESTDomain>> genericEntity = new GenericEntity<JAXBContainer<RESTDomain>>(jAXBContainer){};
return Response.ok(genericEntity).build();
}
Im returning the container with genericEntity. I know with just List inside genericEntity i can get my Entity at my client but the problem is i need to Use my JAXBContainer for some reason.
#Test
public void restGet() throws JAXBException{
ClientConfig cc = new DefaultClientConfig();
client = Client.create(cc);
String baseURI ="http://localhost:3555/SampleREST/rest/sample";
WebResource webResource = client.resource(baseURI);
JAXBContainer<RESTDomain> jAXBContainer= webResource.get(new GenericType<JAXBContainer<RESTDomain>>(){});
System.out.println("response:: "+jAXBContainer.getItemsSize());
}
My problem is im getting the response as JAXBContainer with GenericType as desired but the size of container is 0. What am i missing here? do i have to Use any marshalling and unmarshalling Mechanisms.
But When i request this URI in browser i get the well formed XML, But it fails in client or do we have any other ways to extract entity in client. Thanks in advance
I don't see that you're setting the accept content type anywhere on the client.
Try with: webResource.accept("application/xml")

Preserving model state with Post/Redirect/Get pattern

At the moment I am trying to implement the Post/Redirect/Get pattern with Spring MVC 3.1. What is the correct way to preserve and recover the model data + validation errors? I know that I can preserve the model and BindingResult with the RedirectAttributes in my POST method. But what is the correct way of recovering them in the GET method from the flash scope?
I have done the following to POST:
#RequestMapping(value = "/user/create", method = RequestMethod.POST)
public String doCreate(#ModelAttribute("user") #Valid User user, BindingResult result, RedirectAttributes rA){
if(result.hasErrors()){
rA.addFlashAttribute("result", result);
rA.addFlashAttribute("user", user);
return "redirect:/user";
}
return "redirect:/user/success";
}
And the following to GET the user creation form:
#RequestMapping(value = "/user", method = RequestMethod.GET)
public ModelAndView showUserForm(#ModelAttribute("user") User user, ModelAndView model){
model.addObject("user", user);
model.setViewName("userForm");
return model;
}
This allows me to preserve the given user data in the case of an error. But what is the correct way of recovering the errors?(BindingResult) I'd like to show them in the form with the spring form tags:
<form:errors path="*" />
In addition it would be interesting how to access the flash scope from the get method?
public class BindingHandlerInterceptor extends HandlerInterceptorAdapter {
public static final String BINDING_RESULT_FLUSH_ATTRIBUTE_KEY = BindingHandlerInterceptor.class.getName() + ".flashBindingResult";
private static final String METHOD_GET = "GET";
private static final String METHOD_POST = "POST";
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
if(METHOD_POST.equals(request.getMethod())) {
BindingResult bindingResult = getBindingResult(modelAndView);
FlashMap outFlash = RequestContextUtils.getOutputFlashMap(request);
if(bindingResult == null || ! bindingResult.hasErrors() || outFlash == null ) {
return;
}
outFlash.put(BINDING_RESULT_FLUSH_ATTRIBUTE_KEY, bindingResult);
}
Map<String, ?> inFlash = RequestContextUtils.getInputFlashMap(request);
if(METHOD_GET.equals(request.getMethod()) && inFlash != null && inFlash.containsKey(BINDING_RESULT_FLUSH_ATTRIBUTE_KEY)) {
BindingResult flashBindingResult = (BindingResult)inFlash.get(BINDING_RESULT_FLUSH_ATTRIBUTE_KEY);
if(flashBindingResult != null) {
BindingResult bindingResult = getBindingResult(modelAndView);
if(bindingResult == null) {
return;
}
bindingResult.addAllErrors(flashBindingResult);
}
}
}
public static BindingResult getBindingResult(ModelAndView modelAndView) {
if(modelAndView == null) {
return null;
}
for (Entry<String,?> key : modelAndView.getModel().entrySet()) {
if(key.getKey().startsWith(BindingResult.MODEL_KEY_PREFIX)) {
return (BindingResult)key.getValue();
}
}
return null;
}
}
Why don't you show the update form after the binding fails, so the user can try to resubmit the form?
The standard approach for this seems to be to return the update form view from the POST handler method.
if (bindingResult.hasErrors()) {
uiModel.addAttribute("user", user);
return "user/create";
}
You can then display errors with the form:errors tag.
what is the correct way of recovering them in the GET method from the
flash scope
I'm not sure I understand what you mean by recovering them. What you add as flash attributes before the redirect will be in the model after the redirect. There is nothing special that needs to be done for that. I gather you're trying to ask something else but I'm not sure what that is.
As phahn pointed out why do you redirect on error? The common way to handle this is to redirect on success.