How to use debugger with HTTP.jl

I’ve been struggling to use the new debugging tools with HTTP but I can’t get it to work. Take this simple example:

using HTTP, Sockets

function handleReq(req)
  "200 OK"
end

HTTP.serve(ip"127.0.0.1", 8000, verbose = true, rate_limit = nothing) do req::HTTP.Request
  handleReq(req)
end

I tried everything I could think of but I can’t get the debugger session to start when entering the handler or on error.

1 Like
julia> using HTTP, Sockets, Debugger

julia> function handleReq(req)
         error("nope")
         "200 OK"
       end
handleReq (generic function with 1 method)

julia> Debugger.break_on(:error)

julia> HTTP.serve(ip"127.0.0.1", 8000, verbose = true, rate_limit = nothing) do req::HTTP.Request
         @run handleReq(req)
       end
[ Info: Listening on: 127.0.0.1:8000
[ Info: Accept (1):  🔗    0↑     0↓    1s 127.0.0.1:8000:8000 ≣16
Breaking for error:
ERROR: nope
Stacktrace:
 [1] error(::String) at error.jl:33
 [2] handleReq(::HTTP.Messages.Request) at REPL[2]:2

In error(s) at error.jl:33
>33  error(s::AbstractString) = throw(ErrorException(s))

About to run: (throw)(ErrorException("nope"))
1|debug> bt
[1] error(s) at error.jl:33
  | s::String = "nope"
[2] handleReq(req) at REPL[2]:2
  | req::HTTP.Messages.Request = <suppressed 344 bytes of output>

The important thing to remember is that you need to have a @run or @enter statement somewhere for the debugger to become active.

4 Likes

Excellent, thanks so much!

Although it technically works, it seems to fail consistently in real-life scenarios.

My objective was to get this working with Genie apps, where this handleReq function does a lot of complex work. So it would be something like:

function handleReq()
  response = a_lot_of_deep_nested_computations()
  return response 
end

When passing this through the debugger, two things happen:

1 - it is unbearably slow, to the point of not being usable;

2 - breaking on error, deep in the computation, starts a debug session. Quitting the debug session returns nothing. Which raises another exception because response should not be nothing. Which is being caught again, and so on - it just never seems to exit the debugger which is amplified by being so slow and unresponsive to the point of having to quit the whole thing.


So another approach I took was to load the debugger a lot later within the a_lot_of_deep_nested_computations() stack. Something in the line of:

function a_lot_of_deep_nested_computations()
  a(
    b(
      c(
        # more stuff happening
        Debugger.@run user_generated_code_to_be_debugged()
      )
    )
  )
end

That works much better, performance wise, but the debugger goes nuts. It doesn’t properly process the input, any input is interpreted like garbage:

1|debug>
Unknown command `lphfea`. Executing `?` to obtain help.

1|debug> lphttt?
Unknown command `lphttt?`. Executing `?` to obtain help.

Sometimes it falls down to the julian REPL with garbage, then back to debug:

julia> i_tsss
ERROR: UndefVarError: i_tsss not defined

1|debug>
Unknown command `lphfea`. Executing `?` to obtain help.

I can’t comment on the technicalities as I’m not familiar with the debugger (nor with the debugging process too much), but I guess we can conclude that:

a. it doesn’t seem to work well across spawned processes;

b. it’s not flexible enough - due to its performance penalty, there should be a global flag to disable all debugging. For instance, I’d be happy to use it in development but automatically disable it in production. A global flag like Revise has would be very useful, which would turn the call to @run to doing nothing. This would allow having @run in the codebase without the risk of breaking the app in production or having to chase down all the calls before release.

c. there’s an issue with returning nothing from the debug session - this raises more exceptions if the calling function does not expect nothing.

d. the interpreter is still too slow for randomly complex computations.

e. a @debug helper to make code debuggable would be useful. For instance, in a module like this, which is a Controller, the index() function is automatically invoked by the framework:

module BooksController

using Debugger
Debugger.break_on(:error)

function index()
  a()
  b()
  c()
end

# more code

To make it debuggable I need to do something like:

function index()
  function debuggable_index()
    a()
    b()
    c()
  end
  Debugger.@run debuggable_index()
end

I would be nice to just say @debug and have the function modified for debugging:

@debug function index()
  # code
end

Thanks, I hope it makes sense.

That’s a known issue I think (and might also be where the junk in your REPL comes from).

That seems like something you should be able to code yourself? Or just write a convenience macro that looks at a global const to enable/disable interpreting your code.

If you take a look at the help you’ll see both

  - c: continue execution until a breakpoint is hit
  - q: quit the debugger, returning nothing

so try the c instead of q (which ungracefully shuts down everything, basically).

True. This will get better if someone has time to work on performance (although the interpreter typically isn’t that slow there probably are edge cases where you’ll see a slow down of a couple of orders of magnitude), but in the meantime you can play around with toggling compiled mode with

 - C: toggle compiled mode

Do note that breakpoints don’t work in compiled mode, so it’s only useful for stepping.

@pfitzseb Thank you!

Sure, happy to contribute - just wanted to make sure that it’s not something isolated to my instance or me doing something wrong.

I’ve also added a point e., the @debug macro. Do you think that makes sense or am I approaching the debugging process incorrectly and there’s an easier way to make a function debuggable when the function can not be called directly.

Yeah, right now that’s probably the best way to do this. I don’t think there’s a fundamental reason why @run can’t support arbitrary expressions, including

function index()
  @run begin
    foo()
    bar()
  end
end

blocks.

In general I’d suggest having the @run invocation further towards the original entry point, but if performance is prohibitively bad (do open an issue with a MWE if that’s the case!) then you don’t have much of a choice.

Makes sense, thank you!