Skip to content

Commit

Permalink
Added error handling functionality
Browse files Browse the repository at this point in the history
If the app has an error_handler function defined, then error handler kicks in, otherwise no change.

The intent is to automatically handle errors in most cases, while allowing app code to handle errors explicitly when required. After a plugin API call, if error or truth status is checked, that clears the error state. Otherwise, ret.value call or the next plugin API call will raise an error. So if the app code explicitly checks error status, then no automatic error handling is done. If not, the error is passed to the error_handler function. The error handler function can do a retarget for partial requests and return an error page for non-HTMX requests.

For dev apps, the source location is included in the error message.
  • Loading branch information
akclace committed Feb 29, 2024
1 parent 4e9cc71 commit 6447af1
Show file tree
Hide file tree
Showing 16 changed files with 586 additions and 206 deletions.
1 change: 1 addition & 0 deletions internal/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ type App struct {

globals starlark.StringDict
appDef *starlarkstruct.Struct
errorHandler starlark.Callable
appRouter *chi.Mux
template *template.Template
watcher *fsnotify.Watcher
Expand Down
138 changes: 89 additions & 49 deletions internal/app/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ func (a *App) createHandlerFunc(html, block string, handler starlark.Callable, r
}

// Save the request context in the starlark thread local
thread.SetLocal(TL_CONTEXT, r.Context())
thread.SetLocal(utils.TL_CONTEXT, r.Context())
thread.SetLocal(utils.TL_REQUEST, r)
thread.SetLocal(utils.TL_RESPONSE_WRITER, w)
thread.SetLocal(utils.TL_APP, a)

isHtmxRequest := r.Header.Get("HX-Request") == "true" && !(r.Header.Get("HX-Boosted") == "true")

Expand All @@ -75,7 +78,7 @@ func (a *App) createHandlerFunc(html, block string, handler starlark.Callable, r
pagePath = ""
}
appUrl := getRequestUrl(r) + appPath
requestData := Request{
requestData := utils.Request{
AppName: a.Name,
AppPath: appPath,
AppUrl: appUrl,
Expand Down Expand Up @@ -119,7 +122,9 @@ func (a *App) createHandlerFunc(html, block string, handler starlark.Callable, r
}

defer deferredCleanup()
thread.SetLocal(utils.TL_REQUEST_DATA, requestData)

// Call the handler function
ret, err := starlark.Call(thread, handler, starlark.Tuple{requestData}, nil)
if err != nil {
a.Error().Err(err).Msg("error calling handler")
Expand All @@ -136,41 +141,47 @@ func (a *App) createHandlerFunc(html, block string, handler starlark.Callable, r
}

msg := err.Error()
if firstFrame != "" {
if firstFrame != "" && a.IsDev {
msg = msg + " : " + firstFrame
}
http.Error(w, msg, http.StatusInternalServerError)
return
}

retStruct, ok := ret.(*starlarkstruct.Struct)
if ok {
// Handle Redirect response
url, err := util.GetStringAttr(retStruct, "url")
// starlark Type() is not implemented for structs, so we can't check the type
// Looked at the mandatory properties to decide on type for now
if err == nil {
// Redirect type struct returned by handler
code, err1 := util.GetIntAttr(retStruct, "code")
refresh, err2 := util.GetBoolAttr(retStruct, "refresh")
if err1 != nil || err2 != nil {
http.Error(w, "Invalid redirect response", http.StatusInternalServerError)
}
if a.errorHandler == nil {
// No err handler defined, abort
http.Error(w, msg, http.StatusInternalServerError)
return
}

if refresh {
w.Header().Add("HX-Refresh", "true")
// error handler is defined, call it
valueDict := starlark.Dict{}
valueDict.SetKey(starlark.String("error"), starlark.String(msg))
ret, err = starlark.Call(thread, a.errorHandler, starlark.Tuple{requestData, &valueDict}, nil)
if err != nil {
// error handler itself failed
firstFrame := ""
if evalErr, ok := err.(*starlark.EvalError); ok {
// Iterate through the CallFrame stack for debugging information
for i, frame := range evalErr.CallStack {
fmt.Printf("Function: %s, Position: %s\n", frame.Name, frame.Pos)
if i == 0 {
firstFrame = fmt.Sprintf("Function %s, Position %s", frame.Name, frame.Pos)
}
}
}
a.Trace().Msgf("Redirecting to %s with code %d", url, code)
if deferredCleanup() != nil {
return

msg := err.Error()
if firstFrame != "" && a.IsDev {
msg = msg + " : " + firstFrame
}
http.Redirect(w, r, url, int(code))
http.Error(w, msg, http.StatusInternalServerError)
return
}
}

retStruct, ok := ret.(*starlarkstruct.Struct)
if ok {
// response type struct returned by handler Instead of template defined in
// the route, use the template specified in the response
err, done := a.handleResponse(retStruct, w, requestData, rtype, deferredCleanup)
done, err := a.handleResponse(retStruct, r, w, requestData, rtype, deferredCleanup)
if done {
return
}
Expand All @@ -179,11 +190,14 @@ func (a *App) createHandlerFunc(html, block string, handler starlark.Callable, r
return
}

handlerResponse, err = utils.UnmarshalStarlark(ret)
if err != nil {
a.Error().Err(err).Msg("error converting response")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
if ret != nil {
// Response from handler, or if handler failed, response from error_handler if defined
handlerResponse, err = utils.UnmarshalStarlark(ret)
if err != nil {
a.Error().Err(err).Msg("error converting response")
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
}
}

Expand Down Expand Up @@ -232,81 +246,107 @@ func (a *App) createHandlerFunc(html, block string, handler starlark.Callable, r
return goHandler
}

func (a *App) handleResponse(retStruct *starlarkstruct.Struct, w http.ResponseWriter, requestData Request, rtype string, deferredCleanup func() error) (error, bool) {
func (a *App) handleResponse(retStruct *starlarkstruct.Struct, r *http.Request, w http.ResponseWriter, requestData utils.Request, rtype string, deferredCleanup func() error) (bool, error) {
// Handle ace.redirect type struct returned by handler
url, err := util.GetStringAttr(retStruct, "url")
// starlark Type() is not implemented for structs, so we can't check the type
// Looked at the mandatory properties to decide on type for now
if err == nil {
// Redirect type struct returned by handler
code, err1 := util.GetIntAttr(retStruct, "code")
refresh, err2 := util.GetBoolAttr(retStruct, "refresh")
if err1 != nil || err2 != nil {
http.Error(w, "Invalid redirect response", http.StatusInternalServerError)
}

if refresh {
w.Header().Add("HX-Refresh", "true")
}
a.Trace().Msgf("Redirecting to %s with code %d", url, code)
if deferredCleanup != nil {
if err := deferredCleanup(); err != nil {
return false, err
}
}
http.Redirect(w, r, url, int(code))
return true, nil
}

// Handle ace.response type struct returned by handler
templateBlock, err := util.GetStringAttr(retStruct, "block")
if err != nil {
return err, false
return false, err
}

data, err := retStruct.Attr("data")
if err != nil {
a.Error().Err(err).Msg("error getting data from response")
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, true
return true, nil
}

responseRtype, err := util.GetStringAttr(retStruct, "type")
if err != nil {
a.Error().Err(err).Msg("error getting type from response")
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, true
return true, nil
}
if responseRtype == "" {
// Default to the type set at the route level
responseRtype = rtype
}

if templateBlock == "" && responseRtype != "json" {
return fmt.Errorf("block not defined in response and type is not html"), false
return false, fmt.Errorf("block not defined in response and type is not json")
}

code, err := util.GetIntAttr(retStruct, "code")
if err != nil {
a.Error().Err(err).Msg("error getting code from response")
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, true
return true, nil
}

retarget, err := util.GetStringAttr(retStruct, "retarget")
if err != nil {
a.Error().Err(err).Msg("error getting retarget from response")
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, true
return true, nil
}

reswap, err := util.GetStringAttr(retStruct, "reswap")
if err != nil {
a.Error().Err(err).Msg("error getting reswap from response")
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, true
return true, nil
}

redirect, err := util.GetStringAttr(retStruct, "redirect")
if err != nil {
a.Error().Err(err).Msg("error getting redirect from response")
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, true
return true, nil
}

templateValue, err := utils.UnmarshalStarlark(data)
if err != nil {
a.Error().Err(err).Msg("error converting response")
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, true
return true, nil
}

if strings.ToLower(responseRtype) == "json" {
if deferredCleanup() != nil {
return nil, true
if deferredCleanup != nil && deferredCleanup() != nil {
return true, nil
}
// If the route type is JSON, then return the handler response as JSON
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(templateValue)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return err, true
return true, nil
}
return nil, true
return true, nil
}

requestData.Data = templateValue
Expand All @@ -320,16 +360,16 @@ func (a *App) handleResponse(retStruct *starlarkstruct.Struct, w http.ResponseWr
w.Header().Add("HX-Redirect", redirect)
}

if deferredCleanup() != nil {
return nil, true
if deferredCleanup != nil && deferredCleanup() != nil {
return true, nil
}
w.WriteHeader(int(code))
err = a.template.ExecuteTemplate(w, templateBlock, requestData)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return nil, true
return true, nil
}
return nil, true
return true, nil
}

func getRemoteIP(r *http.Request) string {
Expand Down
Loading

0 comments on commit 6447af1

Please sign in to comment.