I am the admin of an enterprise account at Box, and I'm working on an automated integration to update our users' email addresses and set their quotas, based on our enterprise' internal catalog.
Although the Box API documentation seems targeted at other usage scenarios, I can gather that once I get an access_token/refresh_token pair, that refresh_token is valid for 60 days, and I can get a new one at any time during that period.
Being of the conviction that "something always goes wrong", I'm just wondering if there is any way of automating the initial step of getting an access_token/refresh_token pair, that doesn't require a browser and manual interaction. I'm afraid that IF the refresh_token is lost or becomes invalid due to an update at Box or similar, no one here will remember how you went about getting that initial token pair by hand.
If there isn't a way to do it automatically, I'll just live with it, but I don't want to give up without having asked explicitly to know that I didn't just miss something. :-)
[Is there] any way of automating the initial step of getting an access_token/refresh_token pair, that doesn't require a browser and manual interaction
No, there are no authZ/authN shortcuts. That goes double for accounts that can manage an entire enterprise, given their power and reach.
I'm afraid ... no one here will remember how you went about getting that initial token pair by hand.
One way to resolve this might be to implement something like this:
Create a Box app with the 'manage an enterprise' scope.
Create a web app in your domain that simply implements the OAuth2 workflow.
Store the resulting access/refresh token pair in your persistence layer of choice
If/when something goes wrong due to authZ/authN issues, have your script notify a group email account that someone needs to go to the web app and request a new token.
There are sample web apps available to help get you started. (Python, Asp.NET MVC)
... The Box API documentation seems targeted at other usage scenarios...
A lot of the enterprise-specific stuff is found in the Users and Events parts of the API, and the As-User feature makes the entire API enterprise-ready. It's pretty neat.
You can build a workarround with an webclient like this:
import java.io.IOException;
import java.net.URL;
import java.util.concurrent.ExecutionException;
import com.gargoylesoftware.htmlunit.BrowserVersion;
import com.gargoylesoftware.htmlunit.ElementNotFoundException;
import com.gargoylesoftware.htmlunit.WebClient;
import com.gargoylesoftware.htmlunit.html.HtmlButton;
import com.gargoylesoftware.htmlunit.html.HtmlForm;
import com.gargoylesoftware.htmlunit.html.HtmlPage;
import com.gargoylesoftware.htmlunit.html.HtmlPasswordInput;
import com.gargoylesoftware.htmlunit.html.HtmlSubmitInput;
import com.gargoylesoftware.htmlunit.html.HtmlTextInput;
public class BoxAuth {
private String key;
private String email;
private String password;
private String redirectUrl;
private final String AUTH_URL;
public BoxAuth(String key, String email, String password, String redirectUrl) {
super();
this.key = key;
this.email = email;
this.password = password;
this.redirectUrl = redirectUrl;
this.AUTH_URL = "https://www.box.com/api/oauth2/authorize?response_type=code&client_id=" + key + "&redirect_uri=" + this.redirectUrl;
}
public String authorize() throws IOException, InterruptedException, ExecutionException {
System.out.println("AUTHORIZING: " + AUTH_URL);
final WebClient webClient = new WebClient(BrowserVersion.FIREFOX_17);
HtmlPage loginPage = webClient.getPage(AUTH_URL);
final HtmlPage grantAccessePage = this.authorizeLogin(loginPage);
return this.authorizeGrantAccess(grantAccessePage);
}
private HtmlPage authorizeLogin(HtmlPage page) throws IOException {
final HtmlForm loginForm = page.getFormByName("login_form");
loginForm.getInputByName("password");
final HtmlTextInput emailField = (HtmlTextInput) loginForm.getInputByName("login");
emailField.setValueAttribute(this.email);
final HtmlPasswordInput passwordField = (HtmlPasswordInput) loginForm.getInputByName("password");
passwordField.setValueAttribute(this.password);
final HtmlSubmitInput loginButton = loginForm.getInputByName("login_submit");
final HtmlPage result = loginButton.click();
try {
final HtmlForm test = result.getFormByName("login_form");
throw new Exception("BoxAPI: Wrong login data!!!");
} catch (ElementNotFoundException e) {
}
return result;
}
private String authorizeGrantAccess(HtmlPage grantAccessePage) throws IOException, InterruptedException, ExecutionException {
final HtmlForm grantAccessForm = grantAccessePage.getHtmlElementById("consent_form");
final HtmlButton grantAccess = grantAccessForm.getButtonByName("consent_accept");
final HtmlPage codePage = grantAccess.click();
URL url = codePage.getUrl();
String result = "";
if (url.toString().contains("&code=")) {
result = url.toString().substring(url.toString().indexOf("&code="));
result = result.replace("&code=", "");
}
return result;
}
}
as redirect_url u can use something like "https://app.box.com/services/yourservice"
Related
My aim is to read a CSV file, convert it to JSON and send the generated JSON one by one to ActiveMQ queue. My Code below:
final BindyCsvDataFormat bindy=new BindyCsvDataFormat(camelproject.EquityFeeds.class);
ConnectionFactory connectionFactory = new ActiveMQConnectionFactory("tcp://localhost:61616");
CamelContext _ctx = new DefaultCamelContext();
_ctx.addComponent("jms", JmsComponent.jmsComponentAutoAcknowledge(connectionFactory));
_ctx.addRoutes(new RouteBuilder() {
public void configure() throws Exception {
from("file:src/main/resources?fileName=data-sample.csv")
.unmarshal(bindy)
.marshal()
.json(JsonLibrary.Jackson).log("${body}")
.to("file:src/main/resources/?fileName=emp.json");
}
});
EquityFeeds is my POJO class in the above code.
Issues:
No Output is produced. "emp.json" file does not get generated at the given location.
Also how do I split the generated JSON into individual JSON's and send it to ActiveMQ queue like what I did for XML as below:
.split(body().tokenizeXML("equityFeeds", null)).streaming().to("jms:queue:xml.upstream.queue");
EquityFeeds (POJO):
#CsvRecord(separator = ",",skipFirstLine = true)
public class EquityFeeds {
#DataField(pos = 1)
private String externalTransactionId;
#DataField(pos = 2)
private String clientId;
#DataField(pos = 3)
private String securityId;
#DataField(pos = 4)
private String transactionType;
#DataField(pos = 5)
private Date transactionDate;
#DataField(pos = 6)
private float marketValue;
#DataField(pos = 7)
private String priorityFlag;
// getters and setters...
}
Please kindly help. Please tell me where I am going wrong. Need help desperately. Stuck in this issue and not able to move forward. Any help would be highly appreciated. I have really tried hard, searched Google and tried various options but nothing is working.
Please Note: I commented the .marshal() and .json() to check if the .unmarshal() is working but the unmarshal is also not working as "emp.json" is not getting created.
If nothing happens at all when starting the route then it is most likely due to the relative path you passed to the file component. Probably the execution directory of your Java process is not where you think it is and the file is not found. To simplify things I suggest you start with an absolute path. Once everything else is working figure out the correct relative path (your base should be the value of the user.dir system property).
Re your question about splitting the contents: This is answered in the documentation.
This works for me (Camel 3.1):
public class CsvRouteBuilder extends EndpointRouteBuilder {
#Override
public void configure() {
DataFormat bindy = new BindyCsvDataFormat(BindyModel.class);
from(file("/tmp?fileName=simpsons.csv"))
.unmarshal(bindy)
.split(body())
.log("Unmarshalled model: ${body}")
.marshal().json()
.log("Marshalled to JSON: ${body}")
// Unique file name for the JSON output
.setHeader(Exchange.FILE_NAME, () -> UUID.randomUUID().toString() + ".json")
.to(file("/tmp"));
}
}
// Use lombok to generate all the boilerplate stuff
#ToString
#Getter
#Setter
#NoArgsConstructor
// Bindy record definition
#CsvRecord(separator = ";", skipFirstLine = true, crlf = "UNIX")
public static class BindyModel {
#DataField(pos = 1)
private String firstName;
#DataField(pos = 2)
private String middleName;
#DataField(pos = 3)
private String lastName;
}
Given this input in /tmp/simpsons.csv
firstname;middlename;lastname
Homer;Jay;Simpson
Marge;Jacqueline;Simpson
the log output looks like this
Unmarshalled model: RestRouteBuilder.BindyModel(firstName=Homer, middleName=Jay, lastName=Simpson)
Marshalled to JSON: {"firstName":"Homer","middleName":"Jay","lastName":"Simpson"}
Unmarshalled model: RestRouteBuilder.BindyModel(firstName=Marge, middleName=Jacqueline, lastName=Simpson)
Marshalled to JSON: {"firstName":"Marge","middleName":"Jacqueline","lastName":"Simpson"}
and two json files are written in /tmp.
In my spring boot service, I'm using https://github.com/java-json-tools/json-patch for handling PATCH requests.
Everything seems to be ok except a way to avoid modifying immutable fields like object id's, creation_time etc. I have found a similar question on Github https://github.com/java-json-tools/json-patch/issues/21 for which I could not find the right example.
This blog seems to give some interesting solutions about validating JSON patch requests with a solution in node.js. Would be good to know if something similar in JAVA is already there.
Under many circumstances you can just patch an intermediate object which only has fields that the user can write to. After that you could quite easily map the intermediate object to your entity, using some object mapper or just manually.
The downside of this is that if you have a requirement that fields must be explicitly nullable, you won’t know if the patch object set a field to null explicitly or if it was never present in the patch.
What you can do too is abuse Optionals for this, e.g.
public class ProjectPatchDTO {
private Optional<#NotBlank String> name;
private Optional<String> description;
}
Although Optionals were not intended to be used like this, it's the most straightforward way to implement patch operations while maintaining a typed input. When the optional field is null, it was never passed from the client. When the optional is not present, that means the client has set the value to null.
Instead of receiving a JsonPatch directly from the client, define a DTO to handle the validation and then you will later convert the DTO instance to a JsonPatch.
Say you want to update a user of instance User.class, you can define a DTO such as:
public class UserDTO {
#Email(message = "The provided email is invalid")
private String username;
#Size(min = 2, max = 10, message = "firstname should have at least 2 and a maximum of 10 characters")
private String firstName;
#Size(min = 2, max = 10, message = "firstname should have at least 2 and a maximum of 10 characters")
private String lastName;
#Override
public String toString() {
return new Gson().toJson(this);
}
//getters and setters
}
The custom toString method ensures that fields that are not included in the update request are not prefilled with null values.
Your PATCH request can be as follows(For simplicity, I didn't cater for Exceptions)
#PatchMapping("/{id}")
ResponseEntity<Object> updateUser(#RequestBody #Valid UserDTO request,
#PathVariable String id) throws ParseException, IOException, JsonPatchException {
User oldUser = userRepository.findById(id);
String detailsToUpdate = request.toString();
User newUser = applyPatchToUser(detailsToUpdate, oldUser);
userRepository.save(newUser);
return userService.updateUser(request, id);
}
The following method returns the patched User which is updated above in the controller.
private User applyPatchToUser(String detailsToUpdate, User oldUser) throws IOException, JsonPatchException {
ObjectMapper objectMapper = new ObjectMapper();
// Parse the patch to JsonNode
JsonNode patchNode = objectMapper.readTree(detailsToUpdate);
// Create the patch
JsonMergePatch patch = JsonMergePatch.fromJson(patchNode);
// Convert the original object to JsonNode
JsonNode originalObjNode = objectMapper.valueToTree(oldUser);
// Apply the patch
TreeNode patchedObjNode = patch.apply(originalObjNode);
// Convert the patched node to an updated obj
return objectMapper.treeToValue(patchedObjNode, User.class);
}
Another solution would be to imperatively deserialize and validate the request body.
So your example DTO might look like this:
public class CatDto {
#NotBlank
private String name;
#Min(0)
#Max(100)
private int laziness;
#Max(3)
private int purringVolume;
}
And your controller can be something like this:
#RestController
#RequestMapping("/api/cats")
#io.swagger.v3.oas.annotations.parameters.RequestBody(
content = #Content(schema = #Schema(implementation = CatDto.class)))
// ^^ this passes your CatDto model to swagger (you must use springdoc to get it to work!)
public class CatController {
#Autowired
SmartValidator validator; // we'll use this to validate our request
#PatchMapping(path = "/{id}", consumes = "application/json")
public ResponseEntity<String> updateCat(
#PathVariable String id,
#RequestBody Map<String, Object> body
// ^^ no Valid annotation, no declarative DTO binding here!
) throws MethodArgumentNotValidException {
CatDto catDto = new CatDto();
WebDataBinder binder = new WebDataBinder(catDto);
BindingResult bindingResult = binder.getBindingResult();
binder.bind(new MutablePropertyValues(body));
// ^^ imperatively bind to DTO
body.forEach((k, v) -> validator.validateValue(CatDto.class, k, v, bindingResult));
// ^^ imperatively validate user input
if (bindingResult.hasErrors()) {
throw new MethodArgumentNotValidException(null, bindingResult);
// ^^ this can be handled by your regular exception handler
}
// Here you can do normal stuff with your cat DTO.
// Map it to cat model, send to cat service, whatever.
return ResponseEntity.ok("cat updated");
}
}
No need for Optional's, no extra dependencies, your normal validation just works, your swagger looks good. The only problem is, you don't get proper merge patch on nested objects, but in many use cases that's not even required.
I need to write a web client that hits a legacy web app, logs in to it, pulls down some information from a /widget page, and does some work based off of this page's HTML. I'm choosing to go with a Groovy/HttpBuilder solution, for reasons outside the scope of this question.
The only shortcoming (from what I can tell) is that HttpBuilder doesn't support the retaining of cookies between requests. This is a major problem since the (Java) web app uses JSESSIONID cookies to determine whether or not the user is logged in, has permissions, etc.
So first, if my assertion above is incorrect, and HttpBuilder does support the retaining of cookies across requests, please correct me and perhaps the answer here is a solution that shows me how to tap into this part of HttpBuilder. In this case all of my code below is moot.
Assuming I'm correct and this isn't handled by HttpBuilder, I found this excellent solution that I can't get to work for some reason, hence my question.
My adaptation of that code (see link above) is as follows:
TaskAutomator.groovy
====================
package com.me.myapp.tasker
import groovyx.net.http.ContentType
import groovyx.net.http.Method
class TaskAutomator {
static void main(String[] args) {
TaskAutomator tasker = new TaskAutomator()
String result = tasker.doWork("http://myapp.example.com")
println result
}
String doWork(String baseUrl) {
CookieRetainingHttpBuilder cookiedBuilder = new CookieRetainingHttpBuilder(baseUrl)
Map logins = [username: 'user', password: '12345']
// Go to the main page where we will get back the HTML for a login screen.
// We don't really care about the response here, so long as its HTTP 200.
cookiedBuilder.request(Method.GET, ContentType.HTML, "", null)
// Log in to the app, where, on success, we will get back the HTML for a the
// "Main Menu" screen users see when they log in. We don't really care about
// the response here, so long as its HTTP 200.
cookiedBuilder.request(Method.POST, ContentType.HTML, "/auth", logins)
// Finally, now that our JSESSIONID cookies is authenticated, go to the widget page
// which is what we actually care about interacting with.
def response = cookiedBuilder.request(Method.GET, ContentType.HTML, "/widget", null)
// Test to make sure the response is what I think it is.
print response
String result
// TODO: Now actually do work based off the response.
result
}
}
CookieRetainingHttpBuilder
==========================
package com.me.myapp.tasker
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.Method
class CookieRetainingHttpBuilder {
private String baseUrl
private HTTPBuilder httpBuilder
private List<String> cookies
CookieRetainingHttpBuilder(String baseUrl) {
this.baseUrl = baseUrl
this.httpBuilder = initializeHttpBuilder()
this.cookies = []
}
public def request(Method method, ContentType contentType, String url, Map<String, Serializable> params) {
httpBuilder.request(method, contentType) { request ->
uri.path = url
uri.query = params
headers['Cookie'] = cookies.join(';')
}
}
private HTTPBuilder initializeHttpBuilder() {
def httpBuilder = new HTTPBuilder(baseUrl)
httpBuilder.handler.success = { HttpResponseDecorator resp, reader ->
resp.getHeaders('Set-Cookie').each {
String cookie = it.value.split(';')[0]
cookies.add(cookie)
}
reader
}
httpBuilder
}
}
When I run this code I get the following stack trace (I've culled out un-interesting parts as its pretty large):
Exception in thread "main" groovyx.net.http.HttpResponseException: Not Found
at groovyx.net.http.HTTPBuilder.defaultFailureHandler(HTTPBuilder.java:642)
... (lines omitted for brevity)
at groovyx.net.http.HTTPBuilder$1.handleResponse(HTTPBuilder.java:494)
... (lines omitted for brevity)
at groovyx.net.http.HTTPBuilder.doRequest(HTTPBuilder.java:506)
at groovyx.net.http.HTTPBuilder.doRequest(HTTPBuilder.java:425)
at groovyx.net.http.HTTPBuilder.request(HTTPBuilder.java:374)
at groovyx.net.http.HTTPBuilder$request.call(Unknown Source)
at com.me.myapp.tasker.CookieRetainingHttpBuilder.request(CookieRetainingHttpBuilder.groovy:20)
... (lines omitted for brevity)
at com.me.myapp.tasker.TaskAutomator.doWork(TaskAutomator.groovy:23)
... (lines omitted for brevity)
at com.me.myapp.tasker.TaskAutomator.main(TaskAutomator.groovy:13)
CookieRetainingHttpBuilder:20 is this line from request:
httpBuilder.request(method, contentType) { request ->
Can anyone see why I'm getting this? Additionally, I wanted to confirm my approach/strategy in the TaskAutomater#doWork(...) method. Is my use of CookieRetainingHttpBuilder "correct" in the sense that I'm:
Going to the main/login page
POSTing login creds and logging in
Going to the widget page
Or is there a different way to use HttpBuilder that is better/more efficient here (remember CookieRetainingHttpBuilder is, after all, just a wrapper for HttpBuilder).
I believe that error may be manifesting due to missing imports, or perhaps an older version of HttpBuilder. Looking into HttpBuilder.Class, I see this, which informs my suggestions:
protected java.lang.Object parseResponse(org.apache.http.HttpResponse resp, java.lang.Object contentType) throws groovyx.net.http.HttpResponseException { /* compiled code */ }
I am fairly certain you can just use headers.'Set-Cookiein your httpBuilder setup. The syntax is different from what you have, but the change is small and simple, and this is the base method I use when using HttpBuilder.
#Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7)
import groovyx.net.http.HTTPBuilder
import org.apache.http.HttpException
import static groovyx.net.http.ContentType.TEXT
import static groovyx.net.http.Method.GET
def http = new HTTPBuilder(urlToHit)
http.request(urlToHit, GET, TEXT) { req ->
headers.'User-Agent' = ${userAgent}
headers.'Set-Cookie' = "${myCookie}"
response.success = { resp, reader ->
html = reader.getText()
}
response.failure = { resp, reader ->
System.err.println "Failure response: ${resp.status}"
throw new HttpException()
}
}
Something else to note is that you have no failure handling. I don't know if that will raise an exception, but it could be worth looking into.
EDIT
As suggested, I'm merging my answers (thanks for letting me know...I wasn't sure what proper etiquette was).
Here's what I've come up with. I did my best to reuse the code you posted. I commented as best I could. If you have any questions, let me know.
#Grab(group = 'org.codehaus.groovy.modules.http-builder', module = 'http-builder', version = '0.7')
import static groovyx.net.http.ContentType.HTML
import static groovyx.net.http.Method.POST
import static groovyx.net.http.Method.GET
import groovyx.net.http.ContentType
import groovyx.net.http.HTTPBuilder
import groovyx.net.http.URIBuilder
import groovyx.net.http.Method
import org.apache.http.HttpException
/**
* This class defines the methods used for getting and using cookies
* #param baseUrl The URL we will use to make HTTP requests. In this example, it is https://www.pinterest.com
*/
class CookieRetainingHttpBuilder {
String baseUrl
/**
* This method makes an http request and adds cookies to the array list for later use
* #param method The method used to make the http request. In this example, we use GET and POST
* #param contentType The content type we are requesting. In this example, we are getting HTML
* #param url The URI path for the appropriate page. For example, /login/ is for the login page
* #param params The URI query used for setting parameters. In this example, we are using login credentials
*/
public request (Method method, ContentType contentType, String url, Map<String, Serializable> params) {
List<String> cookies = new ArrayList<>()
def http = new HTTPBuilder(baseUrl)
http.request(baseUrl, method, contentType) { req ->
URIBuilder uriBuilder = new URIBuilder(baseUrl)
uriBuilder.query = params
uriBuilder.path = url
headers.'Accept' = HTML
headers.'User-Agent' = "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/37.0.2049.0 Safari/537.36"
headers.'Set-Cookie' = cookies.join(";")
response.success = { resp, reader ->
resp.getHeaders('Set-Cookie').each {
def cookie = it.value.split(";").toString()
cookies.add(cookie)
}
return reader
}
response.failure = { resp, reader ->
System.err.println "Failure response: ${resp.status}"
throw new HttpException()
}
}
}
}
/**
* This class contains the method to make HTTP requests in the proper sequence
* #param base The base URL
* #param user The username of the site being logged in to
* #param pass The password for the username
*/
class TaskAutomator {
private static String base = "http://myapp.example.com"
private static String user = "thisIsMyUser"
private static String pass = "thisIsMyPassword"
/**
* This method contains the functions in proper order to set cookies and login to a site
* #return response Returns the HTML from the final GET request
*/
static String doWork () {
CookieHandler.setDefault(new CookieManager());
CookieRetainingHttpBuilder cookiedBuilder = new CookieRetainingHttpBuilder(baseUrl: base)
Map logins = [username: user, password: pass]
// Go to the main page where we will get back the HTML for a login screen.
// We don't really care about the response here, so long as its HTTP 200.
cookiedBuilder.request(GET, HTML, "", null)
// Log in to the app, where, on success, we will get back the HTML for a the
// "Main Menu" screen users see when they log in. We don't really care about
// the response here, so long as its HTTP 200.
cookiedBuilder.request(POST, HTML, "/login/", logins)
// Finally, now that our JSESSIONID cookies is authenticated, go to the widget page
// which is what we actually care about interacting with.
def response = cookiedBuilder.request(GET, HTML, "/", null)
// Test to make sure the response is what I think it is.
return response
// TODO: Now actually do work based off the response.
}
}
TaskAutomator tasker = new TaskAutomator()
String result = tasker.doWork()
println result
Is there any way to set the Metadata Description?
https://developer.android.com/reference/com/google/android/gms/drive/Metadata.html#getDescription()
If so, what is the length limit?
I can't see anything in the api: https://developer.android.com/reference/com/google/android/gms/drive/MetadataChangeSet.Builder.html
Unfortunately not at the moment, AFAIK. What I do right now is initializing both GDAA and RESTful API (see the 'trash solution' SO 22295903) like this:
private GoogleApiClient _gac;
private com.google.api.services.drive.Drive _svc;
public GoogleApiClient init(String email){
_gac = new GoogleApiClient.Builder(UT.ACTX).addApi(com.google.android.gms.drive.Drive.API)
.addScope(com.google.android.gms.drive.Drive.SCOPE_FILE).setAccountName(email).build();
com.google.api.client.googleapis.extensions.android.gms.auth.GoogleAccountCredential crd =
GoogleAccountCredential.usingOAuth2(UT.ACTX,
Arrays.asList(com.google.api.services.drive.DriveScopes.DRIVE_FILE));
crd.setSelectedAccountName(email);
_svc = new com.google.api.services.drive.Drive.Builder(
AndroidHttp.newCompatibleTransport(), new GsonFactory(), crd).build();
return this;
}
You get the description from DGAA (GoogleApiClient _gac above), but update/write it to RESTFul like this (off UI thread):
public void oldDescUpW(String titl, String mime, String desc) {
try {
final FileList gLst = _svc.files().list()
.setQ("title = '"+titl+".jpg' and mimeType = '"+mime+"' and trashed = false")
.setFields("items(id)").execute();
if (gLst.getItems().size() == 1) {
final String sId = gLst.getItems().get(0).getId();
com.google.api.services.drive.model.File body =
new com.google.api.services.drive.model.File();
body.setDescription(desc);
_svc.files().patch(sId, body).execute();
}
} catch (Exception e) {}
}
It is also possible to use 'resource ID' from GDAA to address the file in RESTful, but it is not always immediately available (if the file is created in GDAA). See SO 22874657
DISCLAIMER:
It is a HACK and should not stay alive past GDAA delivery of an alternative.
Project: ASP MVC 4 running under .net 4.0 framework:
When running an application under VS 2010 express (or deployed and running under IIS 7.5 on my local machine) the following (pseudocode) result from an action works as expected
[HttpPost]
public ActionResult PostWord(Model model)
{
....
Response.StatusCode = 400;
Return new JsonResult { data = new {fieldName = "Word", error = "Not really a word!" } };
(and I have assigned ContentType and ContentEncoding properties of the JsonResult object, with no difference in behaviour)
When the deployable is moved onto a web host (using IIS 7), firebug is telling me that the response is as expected (400) but there is no JSON in the response (ie there is no text of any kind). If I remove the line
Response.StatusCode = 400;
from the action, the JSON is perfectly formed in the response, but of course the response status code is 200 (OK), which interferes with the consuming javascript and appropriate function call.
Any thoughts on what might be going on and how to fix this? Thank you
I had this exact same problem; in order to make sure that the correct answer is not buried in the comments (as it was for me), I want to reiterate #Sprockincat's comment:
For me at least, it was indeed an issue with IIS Custom errors, and can be solved with:
Response.TrySkipIisCustomErrors = true;
#Sprockincat - you should get credit for this. I'm just making it more visible because it's such a subtle fix to a problem that is quite difficult to diagnose.
I've created a subclass of JsonResult that allows you to specify the HttpStatusCode.
public class JsonResultWithHttpStatusCode : JsonResult
{
private int _statusCode;
private string _statusDescription;
public JsonResultWithHttpStatusCode(object data, HttpStatusCode status)
{
var code = Convert.ToInt32(status);
var description = HttpWorkerRequest.GetStatusDescription(code);
Init(data, code, description);
}
public JsonResultWithHttpStatusCode(object data, int code, string description)
{
Init(data, code, description);
}
private void Init(object data, int code, string description)
{
Data = data;
_statusCode = code;
_statusDescription = description;
}
public override void ExecuteResult(ControllerContext context)
{
context.HttpContext.Response.StatusCode = _statusCode;
context.HttpContext.Response.StatusDescription = _statusDescription;
base.ExecuteResult(context);
}
}
Then you can return this as your result and the status code will get set on the response. You can also test the status code on the result in your tests.
For anyone looking for this - in ASP.NET Core you can set the StatusCode property of JsonResult.
https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.jsonresult.statuscode