DEV Community

JustJinoIT
JustJinoIT

Posted on

Instagram Graph API는 거짓말을 한다: 자동화 파이프라인에서 하루 날린 이야기

Instagram Graph API는 거짓말을 한다: 자동화 파이프라인에서 하루 날린 이야기

Instagram 자동화 파이프라인을 디버깅하는 데 하루를 통째로 썼다. 버그가 있다고 확신했는데, 알고 보니 API가 실패하지 않은 것을 실패했다고 알려주고 있었다.


1. 403이 떠도 게시물은 올라가 있다

media_publish를 호출했더니 이런 응답이 왔다:

{
  "error": {
    "message": "Application request limit reached",
    "type": "OAuthException",
    "code": 4,
    "error_subcode": 2207051,
    "is_transient": false
  }
}
Enter fullscreen mode Exit fullscreen mode

HTTP 403. 실패로 처리하고 DB 상태를 업데이트하지 않았다.

그런데 Instagram에 들어가보니 게시물이 이미 올라가 있었다.

재시도했을 때 중복 게시물이 생기는 걸 보고 알았다. Instagram은 미디어를 게시한 후 403을 반환하는 경우가 있다. 요청이 실패한 게 아니라 성공했는데 응답 코드가 틀린 것이다.

media_publish에서 403을 받으면 실패로 처리하기 전에 최근 미디어를 먼저 확인해야 한다:

async def _publish_container(client, container_id):
    resp = await client.post(
        f"{GRAPH_URL}/{IG_USER_ID}/media_publish",
        params={"creation_id": container_id, "access_token": IG_TOKEN},
    )
    if resp.is_success:
        return resp.json().get("id")

    if resp.status_code == 403:
        await asyncio.sleep(3)
        check = await client.get(
            f"{GRAPH_URL}/{IG_USER_ID}/media",
            params={"fields": "id,timestamp", "limit": 1, "access_token": IG_TOKEN},
        )
        if check.is_success:
            data = check.json().get("data", [])
            if data:
                ts = datetime.fromisoformat(data[0]["timestamp"].replace("Z", "+00:00"))
                if datetime.now(timezone.utc) - ts < timedelta(seconds=60):
                    # 403이었지만 실제로는 게시 성공
                    return data[0]["id"]

    resp.raise_for_status()
Enter fullscreen mode Exit fullscreen mode

2. 일일 한도 안에 있어도 차단된다

Instagram의 공식 발행 한도는 하루 25개다. API로 확인할 수 있다:

r = await client.get(
    f"{GRAPH_URL}/{user_id}/content_publishing_limit",
    params={"fields": "config,quota_usage", "access_token": token}
)
# {"data": [{"config": {"quota_total": 100, "quota_duration": 86400}, "quota_usage": 0}]}
Enter fullscreen mode Exit fullscreen mode

quota_usage는 0이었다. 한도에 전혀 걸리지 않았는데 403이 계속 났다.

원인은 1시간 안에 13개를 올린 것이었다. 일일 한도는 안 넘었지만 봇처럼 보이는 패턴이 별도의 제한을 유발했다. 공식 문서에는 없는 행동 차단이다.

구분하는 방법:

Rate limit 행동 차단
quota_usage 한도 근처 0, 멀쩡해 보임
is_transient 보통 true false
Instagram 앱에 표시 없음 없음
시간 지나면 풀리나 24시간 후 리셋 불명확, 수일 지속 가능

is_transient: false가 핵심 판단 기준이다. 기다려도 풀리지 않는다.

예방책은 루프로 한꺼번에 발행하지 않는 것뿐이다. 반드시 스케줄러로 간격을 두어야 한다.


3. 권한을 추가해도 기존 토큰은 달라지지 않는다

게시물 삭제를 위해 instagram_manage_contents 권한이 필요했다. Meta 개발자 콘솔에서 추가하고 앱 권한 목록에서 확인도 했다. 그리고 삭제를 시도했더니:

{"error": {"message": "(#10) Insufficient permissions", "code": 10}}
Enter fullscreen mode Exit fullscreen mode

여전히 실패. 이유는 알고 나면 당연하다. OAuth 토큰은 발급 시점의 scope를 고정으로 가진다. 앱에 새 권한을 추가해도 기존 토큰은 바뀌지 않는다. 새로 발급받아야 한다.

현재 토큰의 실제 권한 확인:

r = await client.get(
    "https://graph.facebook.com/v22.0/me/permissions",
    params={"access_token": token}
)
granted = [p["permission"] for p in r.json()["data"] if p["status"] == "granted"]
# ['instagram_basic', 'instagram_content_publish', 'instagram_manage_contents']
Enter fullscreen mode Exit fullscreen mode

새 권한이 없으면 Graph API Explorer에서 해당 scope를 체크하고 토큰을 재발급해야 한다.


어디서도 명확하게 문서화되지 않은 내용들이다. 첫 번째 케이스는 중복 게시물이 쌓이는 걸 보기 전까지 오후 내내 다른 데서 원인을 찾고 있었다.

Instagram Graph API v22.0으로 카드뉴스 자동 발행 봇을 만들면서 겪은 내용입니다.

Top comments (0)