Macro Error: invalid next

I’m trying to DRY up some API calls I have in Kemal which are very similar. For example, something like this:

get "/api/posts" do |env|
  env.response.content_type = "application/json"
  env.response.headers["Cache-Control"] = "No-Store"
  if env.request.headers.has_key?("X-API-KEY")
    if env.request.headers["X-API-KEY"] == ENV["API_KEY"]
      data_provider.posts.to_json # This is the only line which differs
    else
      halt env, status_code: 401, response: api_unauthorized_error_json
    end
  else
    halt env, status_code: 400, response: api_missing_key_error_json
  end
end

This pattern works wll, though there’s a lot of repeated code. I tried doing this:

def api_get(env : HTTP::Server::Context, data_proc : Proc(String))
  env.response.content_type = "application/json"
  env.response.headers["Cache-Control"] = "No-Store"
  if env.request.headers.has_key?("X-API-KEY")
    if env.request.headers["X-API-KEY"] == ENV["API_KEY"]
      data_proc.call
    else
      halt env, status_code: 401, response: api_unauthorized_error_json
    end
  else
    halt env, status_code: 400, response: api_missing_key_error_json
  end
end

get "/api/posts" do |env|
  api_get(env, -> { data_provider.posts.to_json })
end

However, that returns this error:

There was a problem expanding macro 'halt'

Code in src/xxx.cr:51:7

 51 | halt env, status_code: 401, response: api_unauthorized_error_json
      ^
Called macro defined in lib/kemal/src/kemal/helpers/macros.cr:80:1

 80 | macro halt(env, status_code = 200, response = "")

Which expanded to:

 > 2 | env.response.print api_unauthorized_error_json
 > 3 | env.response.close
 > 4 | next
       ^
Error: invalid next

Is it not possible what I’m try to do, or am I doing something wrong?

I think the problem is that the halt macro expands to what you’re seeing in the error output, which includes next. Normally when you use halt directly within the route it’s fine since next is what you use to exit a block early. However since you’re using it within your api_get method, next is not valid as it should be using return.

Probably could do something like this instead:

def api_get(env : HTTP::Server::Context, &)
  env.response.content_type = "application/json"
  env.response.headers["Cache-Control"] = "No-Store"

  halt env, status_code: 400, response: api_missing_key_error_json unless env.request.headers.has_key?("X-API-KEY")
  halt env, status_code: 401, response: api_unauthorized_error_json unless env.request.headers["X-API-KEY"] == ENV["API_KEY"]
  
  yield
end

get "/api/posts" do |env|
  api_get env do
    data_provider.posts.to_json
  end
end

This way the halt macro will still be invoked in the context of a block, not a method.

Another option would be to checkout other frameworks that have better ways to handle abstractions like this :wink:.

EDIT: Wouldn’t https://kemalcr.com/guide/#filters make more sense for this kinda stuff, or better yet https://kemalcr.com/guide/#middleware.

1 Like

I looked at the source for that macro and wondered if next was being used to exit the block.

I thought about unraveling the macro in my api_get function and replacing next with return too. It makes it a little more verbose, though still more DRY than repeating that pattern for many api get calls.

It’s interesting you used a block parameter there. I used to use that convention often in Ruby, didn’t really think about using it here in Crystal.

Thanks!

EDIT: Wouldn’t Kemal - Guide 1 make more sense for this kinda stuff, or better yet Kemal - Guide 1.

Yeah, actually the conditional-middleware-execution makes a lot of sense for my use case, since I have a mix of html routes and json api routes. Thanks for pointing that out!