Help

Re: Fetch generated "TypeError : ... not permitted"

Solved
Jump to Solution
2738 1
cancel
Showing results for 
Search instead for 
Did you mean: 
s_kmb
5 - Automation Enthusiast
5 - Automation Enthusiast

Hello !

I am encountering a problem when creating an automation script for one of my tables. 

I set up a trigger to occur when the user creates a record. I then wrote a script to be run as an action of the trigger. This script contains a 'fetch' call to a local Flask server where we will do all the process ensuing this trigger activation.

Unfortunately, whenever I test my script, it always indicates : 

 

TypeError: Requests to '10.110.90.204' are not permitted
    at main on line 1

 

Here is the script I am using :

 

let response = await fetch('https://10.110.90.204:8050/process');

 

Can you help me with this, please ?

1 Solution

Accepted Solutions
ag314
6 - Interface Innovator
6 - Interface Innovator

Right, but that's because 192.168... is on your local network. When you run code from Airtable, you are using their servers. You cannot access a 192.168.. (ie local address) from an Airtable script. You will need Flask running on a public server that you can access. The original code you showed had a public IP address - that's what you need to be using. Which was why I was saying to be VERY, VERY careful when it comes to security and SSL policies. You NEVER want to expose any production servers - especially ones containing important (non-dummy) data - without having a very clear business case.

If you need to access Flask via your local IP address, then one option would be to write a little javascript app on your network that gets the Airtable info you need via Airtable's API and then forwards it along to Flash - and vice-versa. But this is not a clean solution, nor does it let you tie into automation hooks.

If the issues are CORS related, then you may not be able to use automation's fetch method. In which case, you'll want to consider Airtable scripting (outside of an automation) where you can use remoteFetchAsync. 

If you want to elaborate a bit more on your specific use-case, I might be able to suggest some other options.

See Solution in Thread

14 Replies 14
ag314
6 - Interface Innovator
6 - Interface Innovator

Very often a fetch API request requires two components: the url, as you included, and an options object with headers describing the type of request, access credentials, and those sorts of things.

Here’s an example:

let url = YOUR_URL

const options = {
  method: 'GET',
  headers: {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + YOUR_ACCESS_TOKEN
  },
}

let res = await fetch(url, options)

let response = await res.json() // or res.text()
console.log(response)

I’m not too familiar with Flash, but I suspect you may need an "Access-Control-Allow-Origin" header field in the request.

s_kmb
5 - Automation Enthusiast
5 - Automation Enthusiast

Hello ag314, thank you for your reply.

Unfortunately, I still have the same problem. I tried doing the same via the Scripting Extension. When using "fetch()", I have the following error :

ERROR
TypeError: Failed to fetch
    at main on line 10
This error might be related to Cross-Origin Resource Sharing (CORS), a security mechanism that prevents websites from making malicious requests to other sites.

Unfortunately, it also stops some legitimate use-cases (such as calling an external API from the scripting app). You should be able to find some more information about this error from your browser's network developer tools, usually accessed by right-clicking on the page, choosing inspect element, and selecting the console tab in the panel that opens.

If you see notices or errors that refer to CORS or Access-Control-Allow-Origin, it's likely you've encountered a CORS error. The scripting app can't reliably detect CORS errors in all browsers, so you should double-check your code to make sure there aren't other errors (e.g. a mistyped domain name) that could be causing this instead.

You can try using the remoteFetchAsync API or a Scripting Action which both don't run in the browser and therefore don't have CORS limitations. You could also try contacting the API provider to ask about enabling CORS, or post on the community forum to see if there's a workaround for your specific API.

When using remoteFetchAsync(), I have this : 

ERROR
j: Error: Requests to '10.110.89.105' are not permitted
    at main on line 10

 The Flask server I use for testing has the following code :

app = Flask(__name__)
cors = CORS(app)

@app.route("/process", methods=['GET', 'POST']) # used for airtable webhooks -> programming notifications
def process():
    print(request)
    json_string = "webhook received"
    resp = Response(json_string)
    resp.headers['Access-Control-Allow-Origin'] = 'https://airtable.com'
    return resp

if __name__ == '__main__':
    app.run(host='10.110.89.105', port=8050, ssl_context='adhoc', debug=True)

 I do not know what to do from here... I think I am missing something but the question is ... what ?

ag314
6 - Interface Innovator
6 - Interface Innovator

Please paste in the fetch code you are using, including the headers. But be sure to X-out your Bearer token or whatever authorization key is required by Flask.

s_kmb
5 - Automation Enthusiast
5 - Automation Enthusiast

Sure ! Here is the fetch code : 

let url = 'https://10.110.89.105:8050/process';
const options = {
    method : "GET",
    headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin" : "https://airtable.com"
    },
};
let res = await fetch(url, options);
let response = await res.json();
console.log(response);
ag314
6 - Interface Innovator
6 - Interface Innovator

What error are you getting with the code above? Add a console.log(res) right after the res line please. 

s_kmb
5 - Automation Enthusiast
5 - Automation Enthusiast

I added the console.log(res) thing but it says the same thing : "Requests are not permitted".

I think the execution cannot reach the console.log() because of the fetching blocking it with the await.

So I tested without "await" to see what happens, with the following code :

let url = 'https://192.168.1.12:8050/process';
const options = {
    method : "GET",
    headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin" : "https://airtable.com"
    },
    
};
let res = fetch(url, options);
console.log(res);

 And it returned this : 

CONSOLE.LOG
{}

And also the same message as before : 

ERROR
TypeError: Requests to '192.168.1.12' are not permitted
    at main on line 10

 

ag314
6 - Interface Innovator
6 - Interface Innovator

You need the await as fetch is an asynchronous method. So displaying the console.log(res) without it will simply show you an empty res since it hasn't yet had a chance to fetch anything.

Has this ever worked from Airtable to Flask? I'm wondering if there's something on the server side (Flask). Perhaps the self-signed SSL certificate (ssl_context=adhoc) is not being trusted. One thing you could try as a TEST ONLY experiment would be to remove the ssl_context=adhoc argument from the app.run on the server. Again, this would be for TESTING PURPOSES ONLY - you never want to run this way in a live environment.

Also, please be sure the url you are using matches what you enabled in the app.run on the server.

s_kmb
5 - Automation Enthusiast
5 - Automation Enthusiast

I tried again and it's still the same problem.

Flask server :

from flask import Flask, request, Response
from flask_cors import CORS, cross_origin

app = Flask(__name__)

cors = CORS(app)

@app.route("/process", methods=['GET', 'POST']) # used for airtable webhooks -> programming notifications
def process():
    print(request)
    json_string = "webhook received"
    resp = Response(json_string)
    resp.headers['Access-Control-Allow-Origin'] = '*'
    return resp

if __name__ == '__main__':
    app.run(host='192.168.1.12', port=8050, debug=True)

Airtable script : 

let url = 'https://192.168.1.12:8050/process';
const options = {
    method : "GET",
    headers: {
        "Content-Type": "application/json",
        "Access-Control-Allow-Origin" : "https://airtable.com"
    },
    
};
let res = await fetch(url, options);
console.log(res);

And no it has never worked from Airtable to Flask. However, using 'https://192.168.1.12:8050/processin a browser works perfectly. The response is returned and displayed onto the web page.

ag314
6 - Interface Innovator
6 - Interface Innovator

Right, but that's because 192.168... is on your local network. When you run code from Airtable, you are using their servers. You cannot access a 192.168.. (ie local address) from an Airtable script. You will need Flask running on a public server that you can access. The original code you showed had a public IP address - that's what you need to be using. Which was why I was saying to be VERY, VERY careful when it comes to security and SSL policies. You NEVER want to expose any production servers - especially ones containing important (non-dummy) data - without having a very clear business case.

If you need to access Flask via your local IP address, then one option would be to write a little javascript app on your network that gets the Airtable info you need via Airtable's API and then forwards it along to Flash - and vice-versa. But this is not a clean solution, nor does it let you tie into automation hooks.

If the issues are CORS related, then you may not be able to use automation's fetch method. In which case, you'll want to consider Airtable scripting (outside of an automation) where you can use remoteFetchAsync. 

If you want to elaborate a bit more on your specific use-case, I might be able to suggest some other options.

s_kmb
5 - Automation Enthusiast
5 - Automation Enthusiast

Hello,

Yes, I am not surprised at all. I came to suspect this could be a potential reason why it does not work. Thank you for the clarification.

What we need to do is to :

- first, list all the information about some 3D components onto a table (for example, its name, its category, the process used to create it, etc.);

- two, prepare some scripts on our side to work on these components via different API and softwares;

- three, when a change happens on one of the 3D components listed on Airtable, a trigger will activate itself and send a request to our server for it to launch a process via our scripts. This is why I thought the "run a script" action could be of great utility in this case.

I came up with another method : when a change happens on the 3D components table, an automation trigger edits an intermediary table with a status for each component indicating if it needs to be processed or not, and which process need to be used. On our side, I use a Flask scheduler programmed to interrogate the database via the Airtable Python API every X seconds and I check if a component has any ongoing changes thanks to our intermediary table.

For now, I don't see any other ways to address this issue. I am open to any suggestions you could have.

ag314
6 - Interface Innovator
6 - Interface Innovator

How about using an Airtable webhook, which can fire on various things like a change in your table. Then you just need a script on your end to capture the webhook whenever it fires. That would be more efficient than calling an API every five minutes to see if anything changed. You can read more here: https://support.airtable.com/docs/airtable-webhooks-api-overview

s_kmb
5 - Automation Enthusiast
5 - Automation Enthusiast

Hello,

Thank you for your suggestion. I didn't know how to use webhooks at first, but now it's a bit clearer for me.

So, I tried with webhooks. And there is a problem.

My Flask server looks like this now : 

 

 

class BearerAuth(AuthBase):

    def __init__(self, token):
        self.token = token

    def __call__(self, r):
        r.headers["authorization"] = "Bearer " + self.token
        return r
    
app = Flask(__name__)

cors = CORS(app)

webhooks = {}

def getServerIp():
    hostname=socket.gethostname()
    IPAddr=socket.gethostbyname(hostname)
    return IPAddr

def getPort():
    return "8050" # test

def initWebhooks():
    url = f"https://api.airtable.com/v0/bases/{baseId}/webhooks"
    httpGetRequest = requests.get(url=url, auth=BearerAuth(personalToken))
    response_json = httpGetRequest.json()
    for w in response_json["webhooks"]:
        webhooks[w["id"]] = w
    print(response_json)

    keysLength = list(webhooks.keys())
    if len(keysLength) == 0:
        # create a webhook with a notification url to the flask server, so that when something changes on the table, a notification is sent to the server
        createWebhookUrl = f"https://api.airtable.com/v0/bases/{baseId}/webhooks"
        serverUrl = f"https://{ipServer}:{port}/test"
        data = {
            "notificationUrl": serverUrl,
            "specification": {
                "options": {
                    "filters": {
                        "dataTypes": [
                            "tableData"
                        ],
                        "recordChangeScope": tableId,
                        "fromSources": ["client", "publicApi", "automation"],
                        "watchDataInFieldIds": ["fldRiInNzxRPn4iAC", "fld72WfS2WokrK8b0", "fldErLsnLfSGV4ZuZ", "fldAQw2BaIOXmcd9C", 
                                                "fldJAu7jVuzJvp5jJ", "fldbogIDstibYTyuj", "fldjzycHMlwyoAdNt", "fldXZXEWAPRXUx1k4"] 
                    }
                }
            }
        }
        httpPostRequest = requests.post(url=createWebhookUrl, json=data, auth=BearerAuth(personalToken))
        print(httpPostRequest.json()["id"])
        webhooks[httpPostRequest.json()["id"]].append(httpPostRequest.json())
    else:
        # refresh the webhook(s)
        for webhook in webhooks:
            webhookId = webhook
            refreshWebhookUrl = f"https://api.airtable.com/v0/bases/{baseId}/webhooks/{webhookId}/refresh"
            data = {}
            httpPostRequest = requests.post(url=refreshWebhookUrl, data={}, auth=BearerAuth(personalToken))

ipServer = getServerIp()
port = getPort()
initWebhooks()

@app.route("/test", methods=['POST']) # test notification delivery from webhooks
def test():
    # send the response with a 200 or 204 status code with an empty body
    # request the webhooks payload list to get the updates

    # Note: Use the decoded macSecret here, not the Base64-encoded
    # version that was returned from the webhook create API action.
    print(request)
    #hmac = HMAC.new(webhooks[request["webhook"]["id"]]["macSecretBase64"], digestmod=SHA256);
    #hmac.update(bytes(request))
    #expectedContentHmac = 'hmac-sha256=' + hmac.hexdigest()
    
    print("Hey !")
    resp = Response("", status="200")
    # create a thread that will fetch webhooks payloads : the thread will initiate after the response has been returned
    return resp

if __name__ == '__main__':
    app.run(host=ipServer, port=int(port), ssl_context='adhoc', debug=True)

 

 

After the webhook has been created, I did some modifications onto the table to check if a notification has been sent to the Flask server but I do not see any printing on the Flask console. Moreover, it looks like any private IP adresses cannot be used as a notification URL. In the part of the code where I attempt to refresh the webhook, i have this :

 

 

{'webhooks': [{'id': 'XXXXXXXXXXXX', 'specification': {'options': {'filters': {'dataTypes': ['tableData'], 'recordChangeScope': 'XXXXXXXXXXXX', 'fromSources': ['client', 'publicApi', 'automation'], 'watchDataInFieldIds': ['fldRiInNzxRPn4iAC', 'fld72WfS2WokrK8b0', 'fldErLsnLfSGV4ZuZ', 'fldAQw2BaIOXmcd9C', 'fldJAu7jVuzJvp5jJ', 'fldbogIDstibYTyuj', 'fldjzycHMlwyoAdNt', 'fldXZXEWAPRXUx1k4']}}}, 'notificationUrl': 'https://10.110.92.250:8050/test', 'cursorForNextPayload': 9, 'lastNotificationResult': {'success': False, 'completionTimestamp': '2023-05-31T12:14:53.848Z', 'durationMs': 0.330349, 'retryNumber': 6, 'error': {'message': 'The hostname resolves to an invalid or private IP address.'}, 'willBeRetried': True}, 'areNotificationsEnabled': True, 'lastSuccessfulNotificationTime': None, 'isHookEnabled': True, 'expirationTime': '2023-06-07T12:18:14.188Z'}]}

 

 

 Also if I don't use "HTTPS" in the notification URL, it blocks the webhook creation.

 

{'error': {'type': 'INVALID_WEBHOOK_NOTIFICATION_URL', 'message': 'notificationUrl: "http:" not allowed; use "https:".'}}

 

I think it comes back to the same problem I had with connecting to my server via the automation script : if I rely on a private IP address for my server, I cannot communicate with Airtable services. I have the feeling the only way to do so is by using the Python API, but maybe I missed something...

ag314
6 - Interface Innovator
6 - Interface Innovator

I'm sorry, I don't know much about building a flask webhook receiver. But there seems to be a few github projects addressing the topic. Once you have Airtable firing the webhook, the only thing you need to focus on is consuming the webhook. Which takes Airtable out of the equation as far as your middleware is concerned. 

s_kmb
5 - Automation Enthusiast
5 - Automation Enthusiast

Okay, thank you for your reply.

I have started using ngrok to get a public IP address that forwards to the private one. And it works well this way. But I don't know if it's the best thing to do. And I don't know how it plays out in terms of security.

Anyway, thanks a lot for your help and the time you put into answering my questions. It's a lot clearer for me and it helped me educate myself more on certain subjects I was not very proficient on 🙂