Skip to content

Commit f3d4f9f

Browse files
committed
Merge pull request rstudio#1156 from rstudio/barbara/error-hiding
Barbara/error hiding
2 parents 3db7029 + d711f17 commit f3d4f9f

File tree

7 files changed

+219
-28
lines changed

7 files changed

+219
-28
lines changed

NAMESPACE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ export(runGadget)
171171
export(runGist)
172172
export(runGitHub)
173173
export(runUrl)
174+
export(safeError)
174175
export(selectInput)
175176
export(selectizeInput)
176177
export(serverInfo)

R/conditions.R

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,10 @@ withLogErrors <- function(expr,
131131
captureStackTraces(expr),
132132
error = function(cond) {
133133
# Don't print shiny.silent.error (i.e. validation errors)
134-
if (inherits(cond, "shiny.silent.error"))
135-
return()
136-
printError(cond, full = full, offset = offset)
134+
if (inherits(cond, "shiny.silent.error")) return()
135+
if (isTRUE(getOption("show.error.messages"))) {
136+
printError(cond, full = full, offset = offset)
137+
}
137138
}
138139
)
139140
}

R/middleware.R

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -299,9 +299,7 @@ HandlerManager <- R6Class("HandlerManager",
299299

300300
if (reqSize > maxSize) {
301301
return(list(status = 413L,
302-
headers = list(
303-
'Content-Type' = 'text/plain'
304-
),
302+
headers = list('Content-Type' = 'text/plain'),
305303
body = 'Maximum upload size exceeded'))
306304
}
307305
else {
@@ -310,7 +308,18 @@ HandlerManager <- R6Class("HandlerManager",
310308
},
311309
call = .httpServer(
312310
function (req) {
313-
withLogErrors(handlers$invoke(req))
311+
withCallingHandlers(withLogErrors(handlers$invoke(req)),
312+
error = function(cond) {
313+
sanitizeErrors <- getOption('shiny.sanitize.errors', FALSE)
314+
if (inherits(cond, 'shiny.custom.error') || !sanitizeErrors) {
315+
stop(cond$message, call. = FALSE)
316+
} else {
317+
stop(paste("An error has occurred. Check your logs or",
318+
"contact the app author for clarification."),
319+
call. = FALSE)
320+
}
321+
}
322+
)
314323
},
315324
getOption('shiny.sharedSecret')
316325
),
@@ -333,7 +342,7 @@ HandlerManager <- R6Class("HandlerManager",
333342
}
334343

335344
# Catch HEAD requests. For the purposes of handler functions, they
336-
# should be treated like GET. The difference is that they shouldn't
345+
# should be treated like GET. The difference is that they shouldn't
337346
# return a body in the http response.
338347
head_request <- FALSE
339348
if (identical(req$REQUEST_METHOD, "HEAD")) {

R/shiny.R

Lines changed: 19 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,12 @@ NULL
9696
#' an arguably more intuitive arrangement for casual R users, as the name
9797
#' of a function appears next to the srcref where it is defined, rather than
9898
#' where it is currently being called from.}
99+
#' \item{shiny.sanitize.errors}{If \code{TRUE} (the default), then normal
100+
#' errors (i.e. errors not wrapped in \code{safeError}) won't show up in the app;
101+
#' a simple generic error message is printed instead (the error and strack trace
102+
#' printed to the console remain unchanged). If you want this behavior in
103+
#' general, but you DO want a particular error \code{e} to get displayed to the
104+
#' user, please use \code{stop(safeError(e))} instead.}
99105
#' }
100106
#' @name shiny-options
101107
NULL
@@ -593,6 +599,10 @@ ShinySession <- R6Class(
593599

594600
value <- tryCatch(
595601
shinyCallingHandlers(func()),
602+
shiny.custom.error = function(cond) {
603+
if (isTRUE(getOption("show.error.messages"))) printError(cond)
604+
structure(NULL, class = "try-error", condition = cond)
605+
},
596606
shiny.output.cancel = function(cond) {
597607
structure(NULL, class = "cancel-output")
598608
},
@@ -601,18 +611,16 @@ ShinySession <- R6Class(
601611
# path of try, because we don't want it to print. But we
602612
# do want to try to return the same looking result so that
603613
# the code below can send the error to the browser.
604-
structure(
605-
NULL,
606-
class = "try-error",
607-
condition = cond
608-
)
614+
structure(NULL, class = "try-error", condition = cond)
609615
},
610616
error = function(cond) {
611-
msg <- paste0("Error in output$", name, ": ", conditionMessage(cond), "\n")
612-
if (isTRUE(getOption("show.error.messages"))) {
613-
printError(cond)
617+
if (isTRUE(getOption("show.error.messages"))) printError(cond)
618+
if (getOption("shiny.sanitize.errors", FALSE)) {
619+
cond <- simpleError(paste("An error has occurred. Check your",
620+
"logs or contact the app author for",
621+
"clarification."))
614622
}
615-
invisible(structure(msg, class = "try-error", condition = cond))
623+
invisible(structure(NULL, class = "try-error", condition = cond))
616624
},
617625
finally = {
618626
private$sendMessage(recalculating = list(
@@ -898,13 +906,10 @@ ShinySession <- R6Class(
898906
# ..stacktraceon matches with the top-level ..stacktraceoff..
899907
result <- try(shinyCallingHandlers(Context$new(getDefaultReactiveDomain(), '[download]')$run(
900908
function() { ..stacktraceon..(download$func(tmpdata)) }
901-
)))
909+
)), silent = TRUE)
902910
if (inherits(result, 'try-error')) {
903-
cond <- attr(result, 'condition', exact = TRUE)
904-
printError(cond)
905911
unlink(tmpdata)
906-
return(httpResponse(500, 'text/plain; charset=UTF-8',
907-
enc2utf8(conditionMessage(cond))))
912+
stop(attr(result, "condition", exact = TRUE))
908913
}
909914
return(httpResponse(
910915
200,

R/utils.R

Lines changed: 93 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -553,7 +553,13 @@ shinyCallingHandlers <- function(expr) {
553553
return()
554554

555555
handle <- getOption('shiny.error')
556-
if (is.function(handle)) handle()
556+
if (is.function(handle)) {
557+
if ("condition" %in% names(formals(handle))) {
558+
handle(condition = e)
559+
} else {
560+
handle()
561+
}
562+
}
557563
}
558564
)
559565
}
@@ -886,6 +892,87 @@ columnToRowData <- function(data) {
886892
)
887893
}
888894

895+
#' Declare an error safe for the user to see
896+
#'
897+
#' This should be used when you want to let the user see an error
898+
#' message even if the default is to sanitize all errors. If you have an
899+
#' error \code{e} and call \code{stop(safeError(e))}, then Shiny will
900+
#' ignore the value of \code{getOption("shiny.sanitize.errors")} and always
901+
#' display the error in the app itself.
902+
#'
903+
#' @param error Either an "error" object or a "character" object (string).
904+
#' In the latter case, the string will become the message of the error
905+
#' returned by \code{safeError}.
906+
#'
907+
#' @return An "error" object
908+
#'
909+
#' @details An error generated by \code{safeError} has priority over all
910+
#' other Shiny errors. This can be dangerous. For example, if you have set
911+
#' \code{options(shiny.sanitize.errors = TRUE)}, then by default all error
912+
#' messages are omitted in the app, and replaced by a generic error message.
913+
#' However, this does not apply to \code{safeError}: whatever you pass
914+
#' through \code{error} will be displayed to the user. So, this should only
915+
#' be used when you are sure that your error message does not contain any
916+
#' sensitive information. In those situations, \code{safeError} can make
917+
#' your users' lives much easier by giving them a hint as to where the
918+
#' error occurred.
919+
#'
920+
#' @seealso \code{\link{shiny-options}}
921+
#'
922+
#' @examples
923+
#' ## Only run examples in interactive R sessions
924+
#' if (interactive()) {
925+
#'
926+
#' # uncomment the desired line to experiment with shiny.sanitize.errors
927+
#' # options(shiny.sanitize.errors = TRUE)
928+
#' # options(shiny.sanitize.errors = FALSE)
929+
#'
930+
#' # Define UI
931+
#' ui <- fluidPage(
932+
#' textInput('number', 'Enter your favorite number from 1 to 10', '5'),
933+
#' textOutput('normalError'),
934+
#' textOutput('safeError')
935+
#' )
936+
#'
937+
#' # Server logic
938+
#' server <- function(input, output) {
939+
#' output$normalError <- renderText({
940+
#' number <- input$number
941+
#' if (number %in% 1:10) {
942+
#' return(paste('You chose', number, '!'))
943+
#' } else {
944+
#' stop(
945+
#' paste(number, 'is not a number between 1 and 10')
946+
#' )
947+
#' }
948+
#' })
949+
#' output$safeError <- renderText({
950+
#' number <- input$number
951+
#' if (number %in% 1:10) {
952+
#' return(paste('You chose', number, '!'))
953+
#' } else {
954+
#' stop(safeError(
955+
#' paste(number, 'is not a number between 1 and 10')
956+
#' ))
957+
#' }
958+
#' })
959+
#' }
960+
#'
961+
#' # Complete app with UI and server components
962+
#' shinyApp(ui, server)
963+
#' }
964+
#' @export
965+
safeError <- function(error) {
966+
if (inherits(error, "character")) {
967+
error <- simpleError(error)
968+
}
969+
if (!inherits(error, "error")) {
970+
stop("The class of the `error` parameter must be either 'error' or 'character'")
971+
}
972+
class(error) <- c("shiny.custom.error", class(error))
973+
error
974+
}
975+
889976
#' Validate input values and other conditions
890977
#'
891978
#' For an output rendering function (e.g. \code{\link{renderPlot}()}), you may
@@ -979,8 +1066,8 @@ validate <- function(..., errorClass = character(0)) {
9791066
# There may be empty strings remaining; these are message-less failures that
9801067
# started as FALSE
9811068
results <- results[nzchar(results)]
982-
983-
stopWithCondition(c("validation", errorClass), paste(results, collapse="\n"))
1069+
stopWithCondition(c("validation", "shiny.silent.error", errorClass),
1070+
paste(results, collapse="\n"))
9841071
}
9851072

9861073
#' @param expr An expression to test. The condition will pass if the expression
@@ -1087,7 +1174,7 @@ req <- function(..., cancelOutput = FALSE) {
10871174
if (isTRUE(cancelOutput)) {
10881175
cancelOutput()
10891176
} else {
1090-
stopWithCondition("validation", "")
1177+
stopWithCondition(c("validation", "shiny.silent.error"), "")
10911178
}
10921179
}
10931180
}, ...)
@@ -1111,7 +1198,7 @@ req <- function(..., cancelOutput = FALSE) {
11111198
#'
11121199
#' @export
11131200
cancelOutput <- function() {
1114-
stopWithCondition("shiny.output.cancel", "")
1201+
stopWithCondition(c("shiny.output.cancel", "shiny.silent.error"), "")
11151202
}
11161203

11171204
# Execute a function against each element of ..., but only evaluate each element
@@ -1154,7 +1241,7 @@ isTruthy <- function(x) {
11541241
stopWithCondition <- function(class, message) {
11551242
cond <- structure(
11561243
list(message = message),
1157-
class = c(class, 'shiny.silent.error', 'error', 'condition')
1244+
class = c(class, 'error', 'condition')
11581245
)
11591246
stop(cond)
11601247
}

man/safeError.Rd

Lines changed: 82 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/shiny-options.Rd

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)