Grails 2.5 REST PUT not getting called - json

Grails 2.4 RESTful controller.
I have a basic question. I have a RESTful controller with simple domain class and my GET, POST works fine.
How do I send PUT JSON request?
I am using default RESTful generated controllers
url -i -X POST -H "Content-Type: application/json" -d '{"roleId":1,"username":"testuser5"}' http://localhost:8090/testapp/User
HTTP/1.1 201 Created
Server: Apache-Coyote/1.1
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Thu, 03 Jul 2014 02:07:13 GMT
{"id":null,"userId":79,"username":"testuser5"}
Then I tried PUT using same above JSON response (removed id:null and changed the username):
curl -i -X PUT -H "Content-Type: application/json" -d '{"userId":79,"username":"testuser6"}' http://localhost:8090/testapp/User
Request goes to index and I get list of users. What I am doing wrong? How do I invoke "update' method? If I add my own method and I do PUT, my own method gets invoked.
Domain class:
class User {
Integer userId
String username
static mapping = {
table 'user'
version false
id name:'userId', column: 'user_id'
}
static constraints = {
username blank:false, nullable:false
}
}
RESTful controller:
class UserController extends RestfulController {
static responseFormats = ['json', 'xml']
static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
respond User.list(params), model:[userInstanceCount: User.count()]
}
def show(User userInstance) {
respond userInstance
}
def create() {
respond new User(params)
}
#Transactional
def update(User userInstance) {
println "*** in update "
if (userInstance == null) {
notFound()
return
}
if (userInstance.hasErrors()) {
respond userInstance.errors, view:'edit'
return
}
userInstance.save flush:true
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.updated.message', args: [message(code: 'User.label', default: 'User'), userInstance.id])
redirect userInstance
}
'*'{ respond userInstance, [status: OK] }
}
}
protected void notFound() {
request.withFormat {
form multipartForm {
flash.message = message(code: 'default.not.found.message', args: [message(code: 'user.label', default: 'User'), params.id])
redirect action: "index", method: "GET"
}
'*'{ render status: NOT_FOUND }
}
}
}

You're missing the call to user.validate() prior to calling hasErrors(). See
https://github.com/grails/grails-core/blob/master/grails-plugin-rest/src/main/groovy/grails/rest/RestfulController.groovy#L99

Related

How to localize exception message for Json deserialization? An invalid parameter was passed when requesting the interface?

My development environment is.Net7 WebApi (out of the box)
Below is the relevant code. DataAnnotations I have implemented localization.
using System.ComponentModel.DataAnnotations;
namespace WebApi.Dtos
{
public class UserRegistrationDto
{
[Required(ErrorMessage = "UserNameRequiredError")]
[MinLength(6, ErrorMessage = "UserNameMinLengthError")]
[MaxLength(30, ErrorMessage = "UserNameMaxLengthError")]
public required string UserName { get; set; }
[Required(ErrorMessage = "PasswordRequiredError")]
public required string Password { get; set; }
}
}
[HttpPost]
public async Task<IActionResult> RegisterUser([FromBody] UserRegistrationDto userRegistration)
{
return Ok(1);
// IdentityResult userResult = await _userManager.CreateAsync(new IdentityUser { UserName = userRegistration.UserName }, userRegistration.Password);
// return userResult.Succeeded ? StatusCode(201) : BadRequest(userResult);
}
When the request body is invalid JSON.
curl -X 'POST' \
'https://localhost:7177/Authenticate/RegisterUser' \
-H 'accept: */*' \
-H 'Api-Version: 1.0' \
-H 'Content-Type: application/json' \
-d '{}'
{
"$": [
"JSON deserialization for type 'WebApi.Dtos.UserRegistrationDto' was missing required properties, including the following: userName, password"
],
"userRegistration": [
"The userRegistration field is required."
]
}
When the request body is Empty.
curl -X 'POST' \
'https://localhost:7177/Authenticate/RegisterUser' \
-H 'accept: */*' \
-H 'Api-Version: 1.0' \
-H 'Content-Type: application/json' \
-d ''
{
"": [
"The userRegistration field is required."
]
}
It throws exception information before binding to DTO, can this exception information be localized? If not, is it possible to capture this information for secondary processing, such as returning a fixed JSON format?
I've tried this in the Program.cs entry file, but it's not ideal.
.ConfigureApiBehaviorOptions(options =>
{
options.SuppressModelStateInvalidFilter = false;
options.InvalidModelStateResponseFactory = context =>
{
bool knownExceptions = context.ModelState.Values.SelectMany(x => x.Errors).Where(x => x.Exception is JsonException || (x.Exception is null && String.IsNullOrWhiteSpace(x.ErrorMessage) == false)).Count() > 0;
if (knownExceptions)
{
return new BadRequestObjectResult(new { state = false, message = localizer["InvalidParameterError"].Value });
}
// ...
return new BadRequestObjectResult(context.ModelState);
};
})
I have also tried this method, but I can’t capture the exception information that failed when binding DTO alone. They will appear together with the ErrorMessage exception information in DTO like the above writing method.
.AddControllers(options =>
{
// options.Filters.Add(...);
// options.ModelBindingMessageProvider // This doesn't work either, it seems to support [FromForm]
})
Back to the topic, can it be localized? Or there is something wrong with the code. I just learned .Net not long ago. Most of the information I learned came from search engines and official documents. Thanks in advance.
Use the following method.
.AddControllers(options =>
{
// options.ModelBindingMessageProvider.Set...
})
It seems that the exception of JSON deserialization caused by passing invalid parameters can only be eliminated on the client side. So far it seems I haven't found a localization for this exception, but it's not very important to me at the moment.
Thanks #XinranShen for helping point me in the right direction.

"No transformation found: class io.ktor.utils.io.ByteChannelNative" error using Ktor

I'm trying to fetch and deserialize some data that is being hosted on github.
{
"Meals": [
{
"id": "1598044e-5259-11e9-8647-d663bd870b02",
"name": "Tomato pasta",
"quantity": [{
"quantity": 1 },
{
"quantity": 2
},
{
"quantity": 3
}],
"availableFromDate": "1605802429",
"expiryDate": "1905802429",
"info": "Vegetarian",
"hot": false,
"locationLat": 57.508865,
"locationLong": -6.292,
"distance": null
},
{
"id": "2be2d854-a067-43ec-a488-2e69f0f2a624",
"name": "Pizza",
"quantity": [{
"quantity": 1 },
{
"quantity": 2
},
{
"quantity": 3
}
],
"availableFromDate": "1605802429",
"expiryDate": "1905902429",
"info": "Meat",
"hot": false,
"locationLat": 51.509465,
"locationLong": -0.135392,
"distance": null
}
]
}
If I spin up a json-server locally then it works perfectly, so I know that my data class is not the problem. However when I try to do the same thing from that github link I get this error:
Error Domain=KotlinException Code=0 "No transformation found: class io.ktor.utils.io.ByteChannelNative -> class kotlin.collections.List
I have a feeling it might be to do with setting a ContentType or something along those lines but I haven't had any success specifying that so far.
Here is my code to make the request:
class MealApi {
private val httpClient = HttpClient {
install(JsonFeature) {
val json = Json { ignoreUnknownKeys = true }
serializer = KotlinxSerializer(json)
}
}
suspend fun getAllMeals(): List<Meal> {
return httpClient.get(endpoint)
}
}
and here is my data class just for completeness:
#Serializable
data class Meal(
#SerialName("id")
val id: String,
#SerialName("name")
val name: String,
#SerialName("quantity")
val quantity: List<Quantity>,
#SerialName("availableFromDate")
var availableFromDate: String,
#SerialName("expiryDate")
var expiryDate: String,
#SerialName("info")
val info: String,
#SerialName("hot")
val hot: Boolean,
#SerialName("locationLat")
val locationLat: Float,
#SerialName("locationLong")
val locationLong: Float,
#SerialName("distance")
var distance: Double? = null
)
#Serializable
data class Quantity(
#SerialName("quantity")
val quantity: Int
)
UPDATE
I've found that this server https://gitcdn.link/ allows you to serve your raw github files with the right Content-Type.
I've searched a lot how to change the server response headers (to change the plain/text one to application/json) but it seems that ktor actually doesn't allow to do that:
https://youtrack.jetbrains.com/issue/KTOR-617
https://youtrack.jetbrains.com/issue/KTOR-580
A nice way should be to allow the ResponseObserver to change the server response headers and pass through the modify response. But you can't actually.
Your problem depends, as you pointed out, from the fact that the raw github page provides an header Content-Type=plain/text instead of ContentType=application/json.
So IRL when you are running your API in a real server this won't occur as you'll take care to put the right content type at server level.
But if you want a workaround to this you could rewrite your api call in this way:
suspend fun getAllMealsWithFallback() {
var meals: Meals? = null
try {
meals = httpClient.get(endpoint)
} catch (e: NoTransformationFoundException) {
val mealsString: String = httpClient.get(endpoint)
val json = kotlinx.serialization.json.Json {
ignoreUnknownKeys = true
}
meals = json.decodeFromString(mealsString)
} finally {
println("Meals: ${meals?.meals}")
}
}
I had to add this class to conform to the json text you have provided in the github link.
#Serializable
data class Meals(
#SerialName("Meals")
val meals: List<Meal>,
)
Try this:
install(JsonFeature) {
serializer = KotlinxSerializer(KotlinJson { ignoreUnknownKeys = true })
acceptContentTypes = acceptContentTypes + ContentType.Any
}
If you'd like to accept all content types. Or Use ContentType.Text.Any, ContentType.Text.Html if you preferred.
In case the issue is the Content-Type:
You can alter the list of response content types, for which the KotlinxSerializer gets active, by extending the JsonFeature block to:
install(JsonFeature) {
val json = Json { ignoreUnknownKeys = true }
serializer = KotlinxSerializer(json)
receiveContentTypeMatchers += object : ContentTypeMatcher {
override fun contains(contentType: ContentType): Boolean =
contentType == ContentType("text", "plain")
}
}
If you're using Ktor 2.0, you would need ContentNegotiation plugin instead of JsonFeature.
For example if you use Gson:
install(io.ktor.client.plugins.contentnegotiation.ContentNegotiation) {
gson()
}
As a workaround for ktor version 2.0.3 you can create you own AppContentNegotiation class and in the scope.responsePipeline.intercept provide needed contentType
/**
* A plugin that serves two primary purposes:
* - Negotiating media types between the client and server. For this, it uses the `Accept` and `Content-Type` headers.
* - Serializing/deserializing the content in a specific format when sending requests and receiving responses.
* Ktor supports the following formats out-of-the-box: `JSON`, `XML`, and `CBOR`.
*
* You can learn more from [Content negotiation and serialization](https://ktor.io/docs/serialization-client.html).
*/
public class AppContentNegotiation internal constructor(
internal val registrations: List<Config.ConverterRegistration>
) {
/**
* A [ContentNegotiation] configuration that is used during installation.
*/
public class Config : Configuration {
internal class ConverterRegistration(
val converter: ContentConverter,
val contentTypeToSend: ContentType,
val contentTypeMatcher: ContentTypeMatcher,
)
internal val registrations = mutableListOf<ConverterRegistration>()
/**
* Registers a [contentType] to a specified [converter] with an optional [configuration] script for a converter.
*/
public override fun <T : ContentConverter> register(
contentType: ContentType,
converter: T,
configuration: T.() -> Unit
) {
val matcher = when (contentType) {
ContentType.Application.Json -> JsonContentTypeMatcher
else -> defaultMatcher(contentType)
}
register(contentType, converter, matcher, configuration)
}
/**
* Registers a [contentTypeToSend] and [contentTypeMatcher] to a specified [converter] with
* an optional [configuration] script for a converter.
*/
public fun <T : ContentConverter> register(
contentTypeToSend: ContentType,
converter: T,
contentTypeMatcher: ContentTypeMatcher,
configuration: T.() -> Unit
) {
val registration = ConverterRegistration(
converter.apply(configuration),
contentTypeToSend,
contentTypeMatcher
)
registrations.add(registration)
}
private fun defaultMatcher(pattern: ContentType): ContentTypeMatcher =
object : ContentTypeMatcher {
override fun contains(contentType: ContentType): Boolean =
contentType.match(pattern)
}
}
/**
* A companion object used to install a plugin.
*/
#KtorDsl
public companion object Plugin : HttpClientPlugin<Config, AppContentNegotiation > {
public override val key: AttributeKey<AppContentNegotiation> =
AttributeKey("ContentNegotiation")
override fun prepare(block: Config.() -> Unit): AppContentNegotiation {
val config = Config().apply(block)
return AppContentNegotiation(config.registrations)
}
override fun install(plugin: AppContentNegotiation, scope: HttpClient) {
scope.requestPipeline.intercept(HttpRequestPipeline.Transform) { payload ->
val registrations = plugin.registrations
registrations.forEach { context.accept(it.contentTypeToSend) }
if (subject is OutgoingContent || DefaultIgnoredTypes.any { it.isInstance(payload) }) {
return#intercept
}
val contentType = context.contentType() ?: return#intercept
if (payload is Unit) {
context.headers.remove(HttpHeaders.ContentType)
proceedWith(EmptyContent)
return#intercept
}
val matchingRegistrations =
registrations.filter { it.contentTypeMatcher.contains(contentType) }
.takeIf { it.isNotEmpty() } ?: return#intercept
if (context.bodyType == null) return#intercept
context.headers.remove(HttpHeaders.ContentType)
// Pick the first one that can convert the subject successfully
val serializedContent = matchingRegistrations.firstNotNullOfOrNull { registration ->
registration.converter.serialize(
contentType,
contentType.charset() ?: Charsets.UTF_8,
context.bodyType!!,
payload
)
} ?: throw ContentConverterException(
"Can't convert $payload with contentType $contentType using converters " +
matchingRegistrations.joinToString { it.converter.toString() }
)
proceedWith(serializedContent)
}
scope.responsePipeline.intercept(HttpResponsePipeline.Transform) { (info, body) ->
if (body !is ByteReadChannel) return#intercept
if (info.type == ByteReadChannel::class) return#intercept
// !!!!!!! Provide desired content type here
val contentType = <HERE> // default implementation is - context.response.contentType() ?: return#intercept
val registrations = plugin.registrations
val matchingRegistrations = registrations
.filter { it.contentTypeMatcher.contains(contentType) }
.takeIf { it.isNotEmpty() } ?: return#intercept
// Pick the first one that can convert the subject successfully
val parsedBody = matchingRegistrations.firstNotNullOfOrNull { registration ->
registration.converter
.deserialize(context.request.headers.suitableCharset(), info, body)
} ?: return#intercept
val response = HttpResponseContainer(info, parsedBody)
proceedWith(response)
}
}
}
}
And then install it for HttpClient
HttpClient {
install(AppContentNegotiation) {
json(json)
addDefaultResponseValidation()
}
}
In my case, this exception was thrown when I was attempting to access the body() potion of the HTTP response as such:
val httpResponse = httpClient.post(urlString) { ... }
val body = httpResponse.body<YourExpectedSerializableResponseType>
In the happy path scenario, the server would return a body that matched YourExpectedSerializableResponseType and everything would work as expected.
However, for a particular edge case that returned a different (still considered successful) status code, the server returned an empty response body. Since the client was expecting a response body and in this case, there wasn't any, this exception was thrown because it could not serialize an empty response body to the expected type YourExpectedSerializableResponseType.
My recommendation: In addition to ensuring your server is returning the type you expect to serialize/consume on your client, confirm your server is actually returning an object.
Internal Dialogue: I wonder if this exception could be more clear in this case as the issue is more so that the client expected a response body to exist and less so that an empty response body ("") couldn't be serialized into an expected type - especially given that an empty response body isn't even valid JSON. Hm. 🤔

How to POST an object with its childrens ? grails 3 RESTful - security - POST save new objects

I am trying to do a simple POST to save a new object
class diagram
I want to POST a Question with the following attributes:
String name
static hasOne = [questionType: QuestionType]
Each Question has one abstract QuestionType extended by one of this objects:
Mcq (Multiple Choice Question)
SourceCode
Matching
...
For my Mcq, I have the following attributes:
static belongsTo = [questionType: QuestionType]
static hasMany = [propositions: Proposition]
static constraints = {
}
static mapping = {
propositions cascade: 'all-delete-orphan'
}
So my Mcq owns 0 to many Proposition
A Proposition is made of:
String body
Boolean isCorrect
String reason
I am using Postman to check my API. I don't have any UI for now.
My Question, Qcm and Proposition objects have a controller and a service to manage them.
My QuestionService:
class QuestionService {
def save(Question question) {
if (question == null) {
transactionStatus.setRollbackOnly()
render status: NOT_FOUND
return
}
if (question.hasErrors()) {
transactionStatus.setRollbackOnly()
respond question.errors, view:'create'
return
}
question.save flush:true
}
def update(Question question) {
if (question == null) {
transactionStatus.setRollbackOnly()
render status: NOT_FOUND
return
}
if (question.hasErrors()) {
transactionStatus.setRollbackOnly()
respond question.errors, view:'edit'
return
}
question.save flush:true
}
def delete(Question question) {
if (question == null) {
transactionStatus.setRollbackOnly()
render status: NOT_FOUND
return
}
question.delete flush: true
}
}
My QuestionController:
#Secured('ROLE_ADMIN')
#Transactional(readOnly = true)
class QuestionController {
//spring bean injected for the Controller to access the Service
def questionService
static responseFormats = ['json', 'xml']
static allowedMethods = [save: "POST", update: "PUT", delete: "DELETE"]
//views
def index(Integer max) {
params.max = Math.min(max ?: 10, 100)
respond Question.list(params), model:[questionCount: Question.count()]
}
def show(Question question) {
respond question
}
//actions
def save(Question question){
try { questionService.save question }
catch (e){ respond status: PRECONDITION_FAILED }
respond question, [status: CREATED, view:"show"]
}
def update(Question question) {
try { questionService.update question }
catch (e) { respond status: PRECONDITION_FAILED }
respond question, [status: OK, view:"show"]
}
def delete(Question question) {
try { questionService.delete question }
catch (e){ respond status: PRECONDITION_FAILED }
respond status: OK, view:"index"
}
}
I can get each Proposition, Mcq and Question with my index and show actions.
I don't allow my users to POST a new Proposition / Mcq. If they want to do it, they have to UPDATE the full Question itself. So only my Question can be modified (and modify others), others or in read-only.
My problem is that I can NOT POST this:
{
"name": "name1",
"questionType": {
"propositions": [
{
"body": "body proposition 1",
"isCorrect": true,
"reason": "reason proposition 1"
},
{
"body": "body proposition 2",
"isCorrect": false,
"reason": "reason proposition 2"
}
]
}
}
It always returns me a
[
null
]
But this works:
{
"name": "name1",
"questionType": {
"id": 1
}
}
So, in here, I can refer to a QuestionType already in my database (from my bootstrap) but I don't see how to create in cascade all of my Question, QuestionType, Qcm and Proposition.

Grails 3 and Json: "Request method 'POST' not supported"

I'm porting a working webapp from Grails 2.3 to 3.0.1. When I post the Json string {"command":"ping"} to the server i get the following result:
{"timestamp":1429380750958,"status":405,"error":"Method Not Allowed","exception":"org.springframework.web.HttpRequestMethodNotSupportedException","message":"Request method 'POST' not supported","path":"/rps/request"}
Here's the controller:
import org.grails.web.json.JSONObject
class RequestController {
def jsonManagerService
def index() {
JSONObject json = request.JSON
if(!json){
render "{json or gtfo}"
return
}
render jsonManagerService.parseJson(json)
}
}
Here's the JsonManagerService:
import grails.transaction.Transactional
import org.grails.web.json.JSONObject
#Transactional
class JsonManagerService {
def parseJson(JSONObject o) {
switch (o.command){
case("ping"):
return '{"result":"pong"}'
break;
default:
return '{"result":"unknown command"}'
}
}
}
And here's my UrlMappings.groovy (it's the default one):
class UrlMappings {
static mappings = {
"/$controller/$action?/$id?(.$format)?"{
constraints {
// apply constraints here
}
}
"/"(view:"/index")
"500"(view:'/error')
"404"(view:'/notFound')
}
}
It looks like a Spring related issue. All the search on this matter provided no results. Any idea?
Edit: thanks #dmahapatro, added UrlMappingsgroovy. Corrected the controller, dumb mistake, but result is still the same.

Box API always returns invalid grant_type parameter on obtaining access token

I'm writing my own Box SDK for WP8 to make the most out of Tasks. I am having trouble obtaining an access token. I always get this as a return:
{"error":"invalid_request","error_description":"Invalid grant_type parameter or parameter missing"}
The code (all in C#) I'm using is:
internal const String TokenURL = "https://api.box.com/oauth2/token";
CloudHttpAsync.DownloadResponceStreamAsync
(
CloudHttpAsync.PostAsync
(
TokenURL,
new MemoryStream
(
UTF8Encoding.UTF8.GetBytes
(
HttpUtility.UrlEncode
(
String.Format
(
"grant_type=authorization_code&code={0}&client_id={1}&client_secret={2}&redirect_uri={3}",
Code,
ClientID,
ClientSecret,
RedirectURI
)
)
)
),
null,
null
),
null
).ContinueWith((AsyncStream) =>
{
try
{
if (AsyncStream.Exception != null)
{
TaskSource.TrySetException(AsyncStream.Exception.InnerExceptions);
return;
}
String Result = "";
using (StreamReader Reader = new StreamReader(AsyncStream.Result))
{
Result = Reader.ReadToEnd();
}
BoxAuthToken Token = JsonConvert.DeserializeObject<BoxAuthToken>(Result);
TaskSource.TrySetResult(Token);
}
catch (Exception e)
{
TaskSource.TrySetException(e);
}
});
and
public static Task<HttpWebResponse> PostAsync(String URL, Stream UploadData, IRequestSigner Signer, IProgress<NetworkProgress> Progress)
{
TaskCompletionSource<HttpWebResponse> TaskSource = new TaskCompletionSource<HttpWebResponse>();
HttpWebRequest Request = WebRequest.CreateHttp(URL);
Request.Method = "POST";
if (Signer != null)
{
Signer.Sign(Request).ContinueWith((o) =>
{
if (o.Exception != null)
{
TaskSource.TrySetException(o.Exception.InnerExceptions);
return;
}
UploadDataAsync(Request, UploadData, Progress).ContinueWith((AsyncRequest) =>
{
if (AsyncRequest.Exception != null)
{
TaskSource.TrySetException(AsyncRequest.Exception.InnerExceptions);
return;
}
GetResponceAsync(Request).ContinueWith((AsyncResponce) =>
{
if (AsyncResponce.Exception != null)
{
TaskSource.TrySetException(AsyncResponce.Exception.InnerExceptions);
return;
}
TaskSource.TrySetResult(AsyncResponce.Result);
});
});
});
}
else
{
UploadDataAsync(Request, UploadData, Progress).ContinueWith((AsyncRequest) =>
{
if (AsyncRequest.Exception != null)
{
TaskSource.TrySetException(AsyncRequest.Exception.InnerExceptions);
return;
}
GetResponceAsync(Request).ContinueWith((AsyncResponce) =>
{
if (AsyncResponce.Exception != null)
{
TaskSource.TrySetException(AsyncResponce.Exception.InnerExceptions);
return;
}
TaskSource.TrySetResult(AsyncResponce.Result);
});
});
}
return TaskSource.Task;
}
internal static Task<HttpWebRequest> UploadDataAsync(HttpWebRequest Request, Stream Data, IProgress<NetworkProgress> Progress)
{
TaskCompletionSource<HttpWebRequest> TaskSource = new TaskCompletionSource<HttpWebRequest>();
if (Data.Length != 0)
{
Request.ContentLength = Data.Length;
Request.AllowWriteStreamBuffering = false;
Request.BeginGetRequestStream(new AsyncCallback((IAR) =>
{
try
{
using (Stream UploadStream = Request.EndGetRequestStream(IAR))
{
Int64 Upload = 0;
Int64 TotalUploaded = 0;
Int64 Total = Data.Length;
Byte[] Buffer = new Byte[4096];
while (TotalUploaded < Total)
{
Upload = Data.Read(Buffer, 0, Buffer.Length);
TotalUploaded += Upload;
UploadStream.Write(Buffer, 0, (Int32)Upload);
if (Progress != null)
{
Progress.Report(new NetworkProgress()
{
Operation = NetworkOperation.Uploading,
TotalBytes = Total,
BytesProcessed = TotalUploaded
});
}
}
}
TaskSource.TrySetResult(Request);
}
catch (Exception e)
{
TaskSource.TrySetException(e);
}
}),
null);
}
else
{
TaskSource.TrySetResult(Request);
}
return TaskSource.Task;
}
internal static Task<HttpWebResponse> GetResponceAsync(HttpWebRequest Request)
{
TaskCompletionSource<HttpWebResponse> TaskSource = new TaskCompletionSource<HttpWebResponse>();
Request.BeginGetResponse(new AsyncCallback((IAR) =>
{
try
{
HttpWebResponse Responce = (HttpWebResponse)Request.EndGetResponse(IAR);
TaskSource.TrySetResult(Responce);
}
catch (Exception e)
{
if (e is WebException && (e as WebException).Response.ContentLength > 0)
{
TaskSource.TrySetResult((HttpWebResponse)(e as WebException).Response);
}
else
{
TaskSource.TrySetException(e);
}
}
}),
null);
return TaskSource.Task;
}
public static Task<StreamAndLength> GetResponceStreamAsync(Task<HttpWebResponse> Task)
{
TaskCompletionSource<StreamAndLength> TaskSource = new TaskCompletionSource<StreamAndLength>();
Task.ContinueWith((AsyncHWR) =>
{
if (AsyncHWR.Exception != null)
{
TaskSource.TrySetException(AsyncHWR.Exception.InnerExceptions);
return;
}
HttpWebResponse Responce = AsyncHWR.Result;
TaskSource.TrySetResult( new StreamAndLength() { Stream = Responce.GetResponseStream(), Length = Responce.ContentLength });
});
return TaskSource.Task;
}
public static Task<MemoryStream> DownloadResponceStreamAsync(Task<HttpWebResponse> Task, IProgress<NetworkProgress> Progress)
{
TaskCompletionSource<MemoryStream> TaskSource = new TaskCompletionSource<MemoryStream>();
GetResponceStreamAsync(Task).ContinueWith((AsyncStream) =>
{
if (AsyncStream.Exception != null)
{
TaskSource.TrySetException(AsyncStream.Exception.InnerExceptions);
return;
}
MemoryStream MemStream = new MemoryStream();
MemStream.SetLength(AsyncStream.Result.Length);
Int64 CurrentRead = 0;
Int64 TotalRead = 0;
Int64 Total = AsyncStream.Result.Length;
Byte[] Buffer = new Byte[4096];
using (Stream DownloadStream = AsyncStream.Result.Stream)
while (TotalRead < Total)
{
CurrentRead = DownloadStream.Read(Buffer, 0, Buffer.Length);
MemStream.Write(Buffer, 0, (Int32)CurrentRead);
TotalRead += CurrentRead;
if (Progress != null)
{
Progress.Report(new NetworkProgress()
{
Operation = NetworkOperation.Downloading,
TotalBytes = Total,
BytesProcessed = TotalRead
});
}
}
MemStream.Position = 0;
TaskSource.TrySetResult(MemStream);
});
return TaskSource.Task;
}
internal class StreamAndLength
{
public Stream Stream { get; set; }
public Int64 Length { get; set; }
}
Sorry there is a lot of code, I like to write generically :)
Edit: Raw Responces (ClientID & Client Secret removed)
When URL encoding each value:
POST https://api.box.com/oauth2/token HTTP/1.1
Accept: */*
Content-Length: 196
Accept-Encoding: identity
User-Agent: NativeHost
Host: api.box.com
Connection: Keep-Alive
Cache-Control: no-cache
grant_type=authorization_code&code=JknaLbfT6lAXmey3FLYrp9eg1jMbpFuQ&client_id=[subbed]&client_secret=[subbed]&redirect_uri=https%3a%2f%2fCloudBoxWP8
Return:
HTTP/1.1 400 Bad Request
Server: nginx
Date: Fri, 01 Mar 2013 07:35:22 GMT
Content-Type: application/json
Connection: keep-alive
Set-Cookie: box_visitor_id=51305a3a187f34.52738262; expires=Sat, 01-Mar-2014 07:35:22 GMT; path=/; domain=.box.com
Set-Cookie: country_code=US; expires=Tue, 30-Apr-2013 07:35:22 GMT; path=/
Cache-Control: no-store
Content-Length: 99
{"error":"invalid_request","error_description":"Invalid grant_type parameter or parameter missing"}
When URL encode the entire string:
POST https://api.box.com/oauth2/token HTTP/1.1
Accept: */*
Content-Length: 214
Accept-Encoding: identity
User-Agent: NativeHost
Host: api.box.com
Connection: Keep-Alive
Cache-Control: no-cache
grant_type%3dauthorization_code%26code%3d3ikruv5elfdw3fOP55aMDSX7ybLqBFlA%26client_id%3d[subbed]%26client_secret%3d[subbed]%26redirect_uri%3dhttps%3a%2f%2fCloudBoxWP8
Return
HTTP/1.1 400 Bad Request
Server: nginx
Date: Fri, 01 Mar 2013 07:46:03 GMT
Content-Type: application/json
Connection: keep-alive
Set-Cookie: box_visitor_id=51305cbb339de4.03221876; expires=Sat, 01-Mar-2014 07:46:03 GMT; path=/; domain=.box.com
Set-Cookie: country_code=US; expires=Tue, 30-Apr-2013 07:46:03 GMT; path=/
Cache-Control: no-store
Content-Length: 99
{"error":"invalid_request","error_description":"Invalid grant_type parameter or parameter missing"}
No URL encoding:
POST https://api.box.com/oauth2/token HTTP/1.1
Accept: */*
Content-Length: 190
Accept-Encoding: identity
User-Agent: NativeHost
Host: api.box.com
Connection: Keep-Alive
Cache-Control: no-cache
grant_type=authorization_code&code=2wgIzfqhvIgRtVIp2ZvqZ9X8R5u0QNaf&client_id=[subbed]&client_secret=[subbed]&redirect_uri=https://CloudBoxWP8
Return:
HTTP/1.1 400 Bad Request
Server: nginx
Date: Fri, 01 Mar 2013 07:50:31 GMT
Content-Type: application/json
Connection: keep-alive
Set-Cookie: box_visitor_id=51305dc751d7f5.67064854; expires=Sat, 01-Mar-2014 07:50:31 GMT; path=/; domain=.box.com
Set-Cookie: country_code=US; expires=Tue, 30-Apr-2013 07:50:31 GMT; path=/
Cache-Control: no-store
Content-Length: 99
{"error":"invalid_request","error_description":"Invalid grant_type parameter or parameter missing"}
It's not listed anywhere on the Box API documentation, but the request for retrieving the access token requires the header Content-Type: application/x-www-form-urlencoded
I was also stuck on this part for a while until I found the answer on StackOverflow. I forget the link to it though.
The request/response would be helpful. It looks like you are UrlEncoding the entire query string instead of just each value. Which would be submitted to us as:
grant_type%3Dauthorization_code%26code%3Dxyz%26client_id%3Dxyz%26client_secret%3Dxyz%26redirect_uri%3Dxyz
Instead of:
grant_type=authorization_code&code=xyz&client_id=xyz&client_secret=xyz&redirect_uri=xyz
I think that including the redirect_uri in your request body could be complicating things, particularly because it looks to be set to an invalid value (https://CloudBoxWP8) You might resolve this by setting your app to handle a custom protocol (cloudboxwp8://) and pre-configuring Box to redirect to that when the token is granted.
Register a custom protocol for your WP8 app. For example, cloudboxwp8.
Augment your WP8 app to handle a request for some endpoint on that protocol. For example, cloudboxwp8://tokengranted. Implement your token handling logic here.
Edit your Box application and browse to the OAuth2 paramters section (via Manage a Box Application => Edit Application)
In the redirect_uri field, set the value to the custom protocol and endpoint from step 2. Save your changes.
Remove the redirect_uri from your request body and try your request again.
in Windows phone 8.1 WinRT
Dictionary<string, string> contentList = new Dictionary<string, string>();
contentList.Add("code", code);
contentList.Add("client_id", client_id);
contentList.Add("client_secret", clientSecret);
contentList.Add("redirect_uri", redirectUri);
contentList.Add("grant_type", "authorization_code");
FormUrlEncodedContent content = new FormUrlEncodedContent(contentList);
var response = await client.PostAsync(baseURL, content);
YouTubeAutenticationResponse res = JsonConvert.DeserializeObject<YouTubeAutenticationResponse>(await response.Content.ReadAsStringAsync());
public class YouTubeAutenticationResponse
{
[JsonProperty("access_token")]
public string AccessToken { get; set; }
[JsonProperty("token_type")]
public string TokenType { get; set; }
[JsonProperty("expires_in")]
public string ExpiresIn { get; set; }
[JsonProperty("refresh_token")]
public string RefreshToken { get; set; }
}