Exception when POSTing to activities/state endpoint

317 views
Skip to first unread message

Drew Bertola

unread,
Jun 26, 2014, 4:48:18 AM6/26/14
to learnin...@googlegroups.com

When I POST to the activities/state endpoint, with activityId=<uuid>&agent=<json agent>&registration=<uuid>&stateId=resume, I get an exception returned from learninglocker.  It looks like StateController::store() is looking for attached content, but not finding it...

    //Get the content from the request
    $data
['content_info'] = $this->getAttachedContent('content');


What is the attached content?  I don't see this in the spec here:

https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#stateapi


The stack trace is:

#0 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Support/Facades/Facade.php(211): Illuminate\Foundation\Application->abort(400, '`content` was n...')
 
#1 /Users/drew/Documents/hw_workspace/lrs/learninglocker/app/controllers/xapi/DocumentController.php(109): Illuminate\Support\Facades\Facade::__callStatic('abort', Array)
 
#2 /Users/drew/Documents/hw_workspace/lrs/learninglocker/app/controllers/xapi/DocumentController.php(109): Illuminate\Support\Facades\App::abort(400, '`content` was n...')
 
#3 /Users/drew/Documents/hw_workspace/lrs/learninglocker/app/controllers/xapi/DocumentController.php(84): Controllers\xAPI\DocumentController->getPostContent('content')
 
#4 /Users/drew/Documents/hw_workspace/lrs/learninglocker/app/controllers/xapi/StateController.php(92): Controllers\xAPI\DocumentController->getAttachedContent('content')
 
#5 /Users/drew/Documents/hw_workspace/lrs/learninglocker/app/controllers/xapi/DocumentController.php(44): Controllers\xAPI\StateController->store()
 
#6 [internal function]: Controllers\xAPI\DocumentController->index()
 
#7 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Routing/Controller.php(231): call_user_func_array(Array, Array)
 
#8 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(93): Illuminate\Routing\Controller->callAction('index', Array)
 
#9 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Routing/ControllerDispatcher.php(62): Illuminate\Routing\ControllerDispatcher->call(Object(Controllers\xAPI\StateController), Object(Illuminate\Routing\Route), 'index')
 
#10 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Routing/Router.php(934): Illuminate\Routing\ControllerDispatcher->dispatch(Object(Illuminate\Routing\Route), Object(Illuminate\Http\Request), 'Controllers\\xAP...', 'index')
 
#11 [internal function]: Illuminate\Routing\Router->Illuminate\Routing\{closure}()
 
#12 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Routing/Route.php(105): call_user_func_array(Object(Closure), Array)
 
#13 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Routing/Router.php(1000): Illuminate\Routing\Route->run(Object(Illuminate\Http\Request))
 
#14 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Routing/Router.php(968): Illuminate\Routing\Router->dispatchToRoute(Object(Illuminate\Http\Request))
 
#15 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(738): Illuminate\Routing\Router->dispatch(Object(Illuminate\Http\Request))
 
#16 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(708): Illuminate\Foundation\Application->dispatch(Object(Illuminate\Http\Request))
 
#17 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Http/FrameGuard.php(38): Illuminate\Foundation\Application->handle(Object(Illuminate\Http\Request), 1, true)
 
#18 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Session/Middleware.php(72): Illuminate\Http\FrameGuard->handle(Object(Illuminate\Http\Request), 1, true)
 
#19 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Cookie/Queue.php(47): Illuminate\Session\Middleware->handle(Object(Illuminate\Http\Request), 1, true)
 
#20 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Cookie/Guard.php(51): Illuminate\Cookie\Queue->handle(Object(Illuminate\Http\Request), 1, true)
 
#21 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/stack/builder/src/Stack/StackedHttpKernel.php(23): Illuminate\Cookie\Guard->handle(Object(Illuminate\Http\Request), 1, true)
 
#22 /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Foundation/Application.php(606): Stack\StackedHttpKernel->handle(Object(Illuminate\Http\Request))
 
#23 /Users/drew/Documents/hw_workspace/lrs/learninglocker/public/index.php(49): Illuminate\Foundation\Application->run()
 
#24 {main} [] []


Any ideas?

ja...@ht2.co.uk

unread,
Jun 26, 2014, 5:43:37 AM6/26/14
to learnin...@googlegroups.com
Hi Drew,

I believe LL is looking for a file attachment, and if it isn't found it is looking to a field name 'content' for the document. This falls in line with the CORS method of sending documents, where headers cannot be attached (https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#78-cross-origin-requests) and a post field called content must be used.

What exactly are you trying to store in the state API? The purpose of the state API is to persist document storage across sessions, so the content being sent could be anything from JSON, XML or even a binary files such as PDFs.

James

Drew Bertola

unread,
Jun 26, 2014, 8:31:48 AM6/26/14
to ja...@ht2.co.uk, learnin...@googlegroups.com

On Jun 26, 2014, at 4:43 PM, ja...@ht2.co.uk wrote:

> Hi Drew,
>
> I believe LL is looking for a file attachment, and if it isn't found it is looking to a field name 'content' for the document. This falls in line with the CORS method of sending documents, where headers cannot be attached (https://github.com/adlnet/xAPI-Spec/blob/master/xAPI.md#78-cross-origin-requests) and a post field called content must be used.
>
> What exactly are you trying to store in the state API? The purpose of the state API is to persist document storage across sessions, so the content being sent could be anything from JSON, XML or even a binary files such as PDFs.

Thanks, James.

Isn't that just a workaround for older browsers (IE <10)?

Well, giving that a try...

To explain, we're publishing content using Articulate StoryLine which is stuck on TCAPI v 0.9. Woderful, right? Anyway, I'm trying to translate statements (done) and now activity state (for "resume"). I've got storyline talking to a middle layer which I use to translate and otherwise proxy the storyline requests.

Back to my results of trying the above workaround, I get the odd exception during PUT requests with:

[2014-06-26 11:42:58] production.ERROR: exception 'Symfony\Component\HttpKernel\Exception\HttpException' with message 'Cannot amend existing text/plain document with a string' in /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:875

Seen this before? Any ideas?

Thanks again,
--
Drew

Drew Bertola

unread,
Jun 26, 2014, 10:01:24 AM6/26/14
to ja...@ht2.co.uk, learnin...@googlegroups.com

On Jun 26, 2014, at 7:31 PM, Drew Bertola <drewb...@gmail.com> wrote:
>
> Back to my results of trying the above workaround, I get the odd exception during PUT requests with:
>
> [2014-06-26 11:42:58] production.ERROR: exception 'Symfony\Component\HttpKernel\Exception\HttpException' with message 'Cannot amend existing text/plain document with a string' in /Users/drew/Documents/hw_workspace/lrs/learninglocker/vendor/laravel/framework/src/Illuminate/Foundation/Application.php:875
>
> Seen this before? Any ideas?

OK, it looks like I'm not getting a string but an array from $this->getAttachedContent('content') called in StateController.php on line 90...


//Get the content from the request
$data['content_info'] = $this->getAttachedContent('content');


That looks sane. Anyway, logging $data shows:

Array
(
[activityId] => http://hwlc/course/955
[agent] => stdClass Object
(
[name] => Drew Bertola
[mbox] => drewb...@gmail.com
)

[stateId] => resume
[registration] => 8f33642b-c7bf-4308-9f85-70d737a46879
[content_info] => Array
(
[content] => 2D2C60708090e0j0d0a0f0k0b0g0l040h0m0i0c050~2K1~2G11001i11
[contentType] => text/plain
)

)

Now, I've traced through and it looks like I need a contentType of application/json, but how do I do this if I'm sending the content in a POST var?


For reference, I'm looking at:

setContent( $content_info, $method) starting on line 33 of app/Models/DocumentApi.php which has 2 bits of interest:

switch( $mimeType ){
case "application/json":
...
case "text/plain": <<<< my case.
if( !$this->exists ){
$this->content = $content;
} else {
\App::abort(400, sprintf('Cannot amend existing %s document with a string', $this->contentType) ); <<<< my case
}
break;




Drew Bertola

unread,
Jun 27, 2014, 7:08:01 AM6/27/14
to learnin...@googlegroups.com
Sure enough, if I override the conditional and just allow setting of content for text/plain, everything works as expected.


      case "text/plain":
        $this
->content = $content;
       
/*
        if( !$this->exists ){
        } else {
          \App::abort(400, sprintf('Cannot amend existing %s document with a string', $this->contentType) );
        }
        */
     
break;


Andrew Downes

unread,
Jun 27, 2014, 4:58:28 PM6/27/14
to learnin...@googlegroups.com
Hi Drew,

Before looking at the specific issue you're having I'd like to take a step back and understand the approach you're taking. Am I right to understand that you have your Storyline content communicating via Tin Can 0.9 to an application and then that application is communicating via Tin Can 1.0 to Learning Locker? 

If so, what programming language are you using for that application? Whatever language it is, you should strongly consider using one of the libraries here: http://tincanapi.com/libraries/ This will handle the more complex parts of Tin Can for you and ensure you're following the standard correctly. I imagine this will solve your current issue. 

Can I also ask - will you be releasing your application open source? I can see it would be useful to other Storyline and Learning Locker users. 

Andrew

Drew Bertola

unread,
Jun 28, 2014, 3:54:00 AM6/28/14
to Andrew Downes, learnin...@googlegroups.com
Hi Andrew,

This is for a client and will not be released OSS. The bit from storyline to tincan 1.0 might make for a good blog post or a small little middleware script that I could release.

I am using tinCanPHP from Rustici which handles statements nicely, but I don't believe it does anything for the other pieces of the API - activity/state, profile, etc. Maybe I should look again, but there was nothing in the (scant) docs mentioning it. Looking a bit further, I see some functionality that might do the trick. Thanks for the nudge.

I'm writing this all in php with ZF 1.12 as the framework.


--
Drew
> --
> You received this message because you are subscribed to a topic in the Google Groups "Learning Locker" group.
> To unsubscribe from this topic, visit https://groups.google.com/d/topic/learning-locker/W5SMpJhN1-E/unsubscribe.
> To unsubscribe from this group and all its topics, send an email to learning-lock...@googlegroups.com.
> To post to this group, send email to learnin...@googlegroups.com.
> For more options, visit https://groups.google.com/d/optout.

Andrew Downes

unread,
Jun 28, 2014, 4:57:51 AM6/28/14
to learnin...@googlegroups.com
Yeah TinCanPHP should do everything, it's just not documented. I tend to look at the source code for function names, though I've not actually specifically tried state api with TinCanPHP, only TinCanjs.

In case yiu have problems with state in TinCanPHP heres some code that works: https://github.com/garemoko/moodle-mod_tincanlaunch/blob/master/tincanlaunch/locallib.php#L179

I plan to replace this with TinCanPHP myself at some point though, so I dont really recommend copying that code.

Andrew

Drew Bertola

unread,
Jun 28, 2014, 6:49:16 AM6/28/14
to Andrew Downes, learnin...@googlegroups.com
Hi Andrew,

Here’s a question about trying to retrieve state.  When I don’t have state stored (say the first time an actor launches a course), LL throws an exception and responds with a 204.  It’s in DocumentController around line 127:

  public function documentResponse($data){

    $document = $this->document->find( $this->lrs->_id, $this->document_type, $data ); //find the correct document
    if( !$document ){
      \App::abort(204);
    }


For  a GET request (retrieve state) this should respond with a 200 and, perhaps, an empty string for content.  (7.4 of the 1.0.0 spec).

Would you like a bug filed?

In the meantime, I’m still working on tying in tincanphp’s saveState() and retrieveState().  I’ll pass on my findings.

Drew


--
You received this message because you are subscribed to a topic in the Google Groups "Learning Locker" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/learning-locker/W5SMpJhN1-E/unsubscribe.
To unsubscribe from this group and all its topics, send an email to learning-lock...@googlegroups.com.
To post to this group, send an email to learnin...@googlegroups.com.

Andrew Downes

unread,
Jun 28, 2014, 8:44:37 AM6/28/14
to learnin...@googlegroups.com, mrdo...@hotmail.com
Hi Drew
Actually it should return 404 (see section 7.1) but still 204 is not right. it'd be great if you could raise am issue for that.

Andrew

Drew Bertola

unread,
Jun 28, 2014, 9:00:52 AM6/28/14
to Andrew Downes, learnin...@googlegroups.com
OK, I'll do so. I'm assuming I'm doing something wrong while bringing these issues up, so at least I don't file a lot of silly issues - i.e. discuss first with you.

Here's the next one:

I want to PUT to activities/state. This would be something like
{
agent : <agent obj>
activity : <activityId>
id : <stateId, ex: "resume">
content : <content string, ex: "123098098342039842">
}

TinCanPHP wants to send this by default as application/octet-stream, but there's no attachment. So, I set contentType (via 4th arg "options" not in prototype - UGH!)
- If I set it to text/plain, I run into the abort(400) discussed earlier (DocumentAPI.php, line 76 or so).

- If I set it to application/json, it must be a json object or I end up with my content being nulled out in json_decode (line 48). Really, I just want to send the value. Do I really have to create an object? I thought state was supposed to allow for key value pairs - state ID is the key, value is the value. No?

Thanks,
--
Drew

Andrew Downes

unread,
Jun 28, 2014, 4:45:41 PM6/28/14
to learnin...@googlegroups.com, mrdo...@hotmail.com
It used to be key value pairs, its now key document pairs. Most authoring tools will store a collection of bookmarking data as a single document.  If you only need to update one value you can do this: https://github.com/adlnet/xAPI-Spec/blob/1.0.1/xAPI.md#json-procedure-with-requirements.

Does that help?

Andrew


Reply all
Reply to author
Forward
0 new messages