daranzolin / rcanvas

R Client for Canvas LMS API
Other
90 stars 43 forks source link

create_wpage() gives HTTP error 414 if wiki_page[body] is long. #38

Closed jonovik closed 5 years ago

jonovik commented 5 years ago

create_wpage() with more than about 8000 characters in wiki_page[body] fails with Request-URI Too Long (HTTP 414).

Usually, this error code means we used GET when we meant POST, but rcanvas::create_wpage uses rcanvas:::canvas_query(type = "POST") which in turn uses httr::POST, which I would expect to do the right thing.

As specified by the Canvas Live API
(scroll down to Pages and look for POST /v1/courses/{course_id}/pages),
canvas_query does not use the body= argument to httr::POST; instead, the page body is passed as wiki_page[body].

Here's a small reproducible example illustrating the issue; it would be interesting to test on other people's Canvases. With a wiki_page[body] of 4 characters it works fine, and with 9000 characters (over the usual limit of 8192 bytes) it causes a HTTP 414 error. Curiously, it runs without error in the Live API for my university's Canvas site, creating the page as expected.

library(tidyverse)
library(rcanvas)

body_size <- 4
YOUR_COURSE_TITLE <- "<insert your course title here>"
course_id <- get_course_list() %>% filter(name %>% str_detect(YOUR_COURSE_TITLE)) %>% pull(id)
title <- "Test Request-URI Too Long"
body <- paste(rep("x", body_size), collapse = "")
create_wpage(course_id, title, body)

This succeeds if body_size = 4. We can see that the response object has a long url that contains the wiki_page[body]. (This is also the case when I test using the Live API.)

Page 'Test Request-URI Too Long' created
Response [https://abcd.instructure.com/api/v1/courses/1234/pages?wiki_page%5Btitle%5D=Test%20Request-URI%20Too%20Long&wiki_page%5Bbody%5D=xxxx&wiki_page%5Bediting_roles%5D=teachers&wiki_page%5Bnotify_of_update%5D=FALSE&wiki_page%5Bpublished%5D=FALSE&wiki_page%5Bfront_page%5D=FALSE]
  Date: 2019-08-19 04:43
  Status: 200
  Content-Type: application/json; charset=utf-8
  Size: 793 B

But it fails if body_size = 9000:

Error in canvas_query(url, args, "POST") : 
Request-URI Too Long (HTTP 414).
> devtools::session_info()
Session info ---------------------------------------------------------------------------------------------------------
 setting  value                       
 version  R version 3.6.1 (2019-07-05)
 system   x86_64, mingw32             
 ui       RStudio (1.2.1335)          
 language (EN)                        
 collate  English_United States.1252  
 tz       Europe/Berlin               
 date     2019-08-19                  

Packages -------------------------------------------------------------------------------------------------------------
 package   * version    date       source                            
 base      * 3.6.1      2019-07-05 local                             
 compiler    3.6.1      2019-07-05 local                             
 datasets  * 3.6.1      2019-07-05 local                             
 devtools    1.13.6     2018-06-27 CRAN (R 3.5.1)                    
 digest      0.6.15     2018-01-28 CRAN (R 3.5.1)                    
 graphics  * 3.6.1      2019-07-05 local                             
 grDevices * 3.6.1      2019-07-05 local                             
 magrittr    1.5        2014-11-22 CRAN (R 3.5.1)                    
 memoise     1.1.0      2017-04-21 CRAN (R 3.5.1)                    
 methods   * 3.6.1      2019-07-05 local                             
 rcanvas   * 0.0.0.9001 2019-08-14 local (daranzolin/rcanvas@31f7eab)
 stats     * 3.6.1      2019-07-05 local                             
 tools       3.6.1      2019-07-05 local                             
 utils     * 3.6.1      2019-07-05 local                             
 withr       2.1.2      2018-03-15 CRAN (R 3.5.1)
jonovik commented 5 years ago

I think I have a solution.

The problem

Calling httr::POST(api_url, ..., query = args) does indeed put query into the resulting full url. This is mentioned in ?httr::POST:

... Further named parameters, such as query, path, etc, passed on to modify_url(). Unnamed parameters will be combined with config().

The call signature of httr::POST() is

function(url = NULL, config = list(), ..., body = NULL, encode = c("multipart", "form", "json", "raw"), handle = NULL)

Because query is not among the named arguments here, it ends up in ... and thus gets passed to modify_url when the current create_wpage code calls canvas_query.

Solution

Somewhat counterintuitively, we can pass all the wiki_data[...] information in the body argument to httr::POST.

The following works:

library(tidyverse)
library(rcanvas)

# Based on rcanvas::create_wpage
my_create_wpage <- function(course_id, title, body, editing_roles = "teachers", published = FALSE){
  # POST /api/v1/courses/:course_id/pages
  # wiki_page[notify_of_update] boolean Whether participants should be notified when this page changes.
  # wiki_page[front_page]           boolean Set an unhidden page as the front page (if true)
  url <- paste0(rcanvas:::canvas_url(), file.path("courses", course_id, "pages"))
  args <- rcanvas:::sc(list(`wiki_page[title]` = title,
                  `wiki_page[body]` = body,
                  `wiki_page[editing_roles]` = editing_roles,
                  `wiki_page[notify_of_update]` = FALSE,
                  `wiki_page[published]` = published,
                  `wiki_page[front_page]` = FALSE
  ))
  # resp <- httr::POST(url = url,
  #                    httr::user_agent("rcanvas - https://github.com/daranzolin/rcanvas"),
  #                    httr::add_headers(Authorization = paste("Bearer", rcanvas:::check_token())),
  #                    body = args)
  resp <- httr::POST(url,
      httr::user_agent("rcanvas - https://github.com/daranzolin/rcanvas"),
      httr::add_headers(Authorization = paste("Bearer", rcanvas:::check_token())),
      body = args)

  httr::stop_for_status(resp)
  message(sprintf("Page '%s' created", title))
  return(resp)

}

body_size <- 9000
course_id <- get_course_list() %>% filter(name %>% str_detect("COURSE_NAME")) %>% pull(id)
title <- "Test Request-URI Too Long"
body <- paste(rep("x", body_size), collapse = "")
my_create_wpage(course_id, title, body)

A reasonably clean fix might be to modify canvas_query to pass args as body if the method is POST.