Calling Anvil HTTP endpoint from Javascript on other site

Can someone walk me through a simple example of setting up an HTTP endpoint that could be called from Javascript code on another site? I’m going the route of having a mostly static site with iframes to embed Anvil forms, but I need some Javascript on the static site to call HTTP endpoints on the Anvil app.

I’m getting an error that tells me I need to add the Access-Control-Allow-Origin header, but I’m not sure where to do that in Anvil. Does it happen in the endpoint function itself? Somewhere else?

Here’s the endpoint as it currently stands:

@anvil.server.http_endpoint("/user/logout")
def logout_api(**qs):
  anvil.users.logout()

This endpoint is not getting executed, so I’m suspecting there’s somewhere else I need to respond to the request for the header.

You will need to set the cross site scripting header stuff. This post might help :

@david.wylie Does that happen inside the HTTP endpoint? Or somewhere else?

From my perspective it looks like the endpoint isn’t being called at all (otherwise the user would be logged out).

Yes, that’s inside my API handler. So you deal with the API call (in my example DB updates), then you set up a response object, setting the headers for CORS.

Cross origin checks are done in the browser, so if you are successfully calling your API endpoint then the code should run but your browser will give you a failure as it refuses to process the response.

Give me a minute and I’ll have an example for you … (and I just noticed that you’re not calling from a browser, so example will reflect that)

Ok, here’s an example.

https://anvil.works/build#clone:UCTPPYFRLQXABGHJ=JO2KQWQVV6LX7WTLGLU5UVPV
I have that app live at the following public URL : https://direct-unwieldy-desk.anvil.app/_/api/dave
You should be able to call that from anywhere (including your browser).

My endpoint looks like this :

@anvil.server.http_endpoint("/dave")
def dave_handler(**kwargs):
  r = anvil.server.HttpResponse()
  r.headers['Access-Control-Allow-Origin'] = '*'
  r.headers['Access-Control-Allow-Methods'] = 'POST, PUT, DELETE, GET, OPTIONS'
  r.headers['Access-Control-Request-Method'] = '*'
  r.headers['Access-Control-Allow-Headers'] = 'Origin, X-Requested-With, Content-Type, Accept, Authorization'
  r.status = 200
  rnd = randint(1,100000)
  r.body = "Dave Called Successfully : " + str(rnd)
  app_tables.log.add_row(data="New "+str(rnd))
  
  return r

It inserts a line into the DB to indicate it’s doing something. Try calling it from your JS app as is (it should work), then try commenting out the cors bit and see if it fails.

If the CORS fails when commented out, you should still see a new row in the DB, proving that the endpoint runs even if CORS fails. So, if your endpoint is not running then something else is going wrong.

You can also test from here : https://resttesttest.com/ (just paste the API url, no need to set anything else).
This will fail without the CORS headers set in the response but work fine with them.

1 Like

Played with that a bit, I can see that it is working when the CORS headers are set, but it’s messing with my mind that we’re setting the CORS headers in the same endpoint that the browser will refuse to execute.

The endpoint gets executed even if the browser blocks Javascript from using the result. I just verified that with resttesttest.com (excellent site, thanks for that link!) The row gets added to the data table whether the CORS headers are set or not. So the CORS headers are needed only to stop the Javascript from generating an error on the browser side.

Thanks for the detailed example, that got me past my mental block on thinking there had to be somewhere else we were setting the CORS headers.

My logout endpoint still isn’t logging the user out, but that’s no doubt a different issue entirely.

Update: We have recently improved the @anvil.server.http_endpoint decorator to do most of these things for you! Check out the reference docs here.

In particular, you can now accomplish the same as @david.wylie’s code with:

@anvil.server.http_endpoint("/dave", enable_cors=True)
def dave_handler(**kwargs):
  rnd = randint(1, 100000)
  app_tables.log.add_row(data="New "+str(rnd))
  return "Dave called successfully : " + str(rnd)

We have also added a feature which solves @jshaffstall’s problem, which is that endpoints execute in a separate session from the rest of the app, if they are invoked from outside your app. This is to prevent a nasty set of attacks called XSRF (Cross-Site Request Forgery), where a malicious outsider can cause a logged-in user to make changes to their account that they weren’t really expecting to make!

You can now selectively disable these protections for a particular endpoint, like this:

@anvil.server.http_endpoint("/user/logout", cross_site_session=True)
def logout_api(**qs):
  anvil.users.logout()

This will cause your Python code to execute in the same session as the rest of your app, which means that the anvil.users.logout() call will log the user out.

Note that doing this carries significant security risks - it means that this endpoint may be triggered by malicious sites! (In this case, it means random malicious websites can log your users out. Probably not too bad in this case, but you can imagine what kind of trouble an unprotected /delete-my-account endpoint might invite!)

We’ve written a bit more about the security issues in the HTTP endpoint reference docs, but for a full treatment there are many excellent resources that you should read carefully. The web is full of pitfalls, which Anvil tries hard to protect you from – remove the safety catch at your peril :wink:

2 Likes

After thinking back then about the security issues, I came up with an alternate approach to avoid the open endpoint.

I do appreciate the flexibility, though, and automating the cors headers is a great timesaver.

1 Like

What alternative did you end up going for?

I don’t use user sessions in HTTP endpoints. Instead the endpoints are simple REST APIs that perform actions on data tables, and then users must use the actual Anvil app to login/logout and do other tasks.

1 Like