Breaking out of a loop early

I want to download a file from a set of URLs in a loop, stopping after the first URL that actually has the file. Here’s a sample playbook illustrating the problem:

---
- hosts: localhost
  gather_facts: false
  tasks:
    - name: try to get a file
      uri:
        url: "{{ item }}"
        dest: "/tmp/data.dat"
        mode: "0644"
      register: results
      ignore_errors: true
      loop:
        - "http://localhost/readme2.txt"
        - "http://localhost/readme1.txt"
        - "http://localhost/readme.txt"
      loop_control:
        extended: true
      until:
        - results is defined
        - results.results[ansible_loop.index0].status == 200    # <--- This does not work!

    - name: print the results
      debug:
        var: results

Of course, the real URLs are different, and the file to be downloaded is pretty big, which is why I would like to avoid doing it N times, but do it only once. With the URLs in the example above, the resulting status codes should be 200, 404, and 200, respectively, so the loop should stop after one iteration. But there’s no guarantee that the first URL will yield a 200, it just so happens to be the case in this test scenario.

I’m struggling with getting the conditions right:

If I leave off the ignore_errors, the playbook stops after the loop, and I can’t do any further processing. Also, it still insists on iterating through the list of URLs. If I leave the second condition off, the result looks like this:

ok: [localhost] => {
    "results": {
        "changed": true,
        "failed": true,
        "msg": "One or more items failed",
        "results": [
            {
                "accept_ranges": "bytes",
                "ansible_loop": {
                    "allitems": [
                        "http://localhost/readme2.txt",
                        "http://localhost/readme1.txt",
                        "http://localhost/readme.txt"
                    ],
                    "first": true,
                    "index": 1,
                    "index0": 0,
                    "last": false,
                    "length": 3,
                    "nextitem": "http://localhost/readme1.txt",
                    "revindex": 3,
                    "revindex0": 2
                },
                "ansible_loop_var": "item",
                ...
                "invocation": {
                    "module_args": {
                        "dest": "/tmp/data.dat",
                        "method": "GET",
                        "path": "/tmp/data.dat",
                        "status_code": [
                            200
                        ],
                        "url": "http://localhost/readme2.txt",
                    }
                },
                "item": "http://localhost/readme2.txt",
                "last_modified": "Mon, 12 Mar 2018 13:58:09 GMT",
                "mode": "0644",
                "msg": "OK (105 bytes)",
                "owner": "toni",
                "path": "/tmp/data.dat",
                "redirected": false,
                "server": "nginx",
                "size": 105,
                "state": "file",
                "status": 200,
                "uid": 1000,
                "url": "http://localhost/readme2.txt"
            },
            {
                "ansible_loop": {
                    "allitems": [
                        "http://localhost/readme2.txt",
                        "http://localhost/readme1.txt",
                        "http://localhost/readme.txt"
                    ],
                    "first": false,
                    "index": 2,
                    "index0": 1,
                    "last": false,
                    "length": 3,
                    "nextitem": "http://localhost/readme.txt",
                    "previtem": "http://localhost/readme2.txt",
                    "revindex": 2,
                    "revindex0": 1
                },
                "ansible_loop_var": "item",
                "invocation": {
                    "module_args": {
                        "method": "GET",
                        "status_code": [
                            200
                        ],
                        "url": "http://localhost/readme1.txt",
                    }
                },
                "item": "http://localhost/readme1.txt",
                "msg": "Status code was 404 and not [200]: HTTP Error 404: Not Found",
                "status": 404,
            },
            {
                "accept_ranges": "bytes",
                "ansible_loop": {
                    "allitems": [
                        "http://localhost/readme2.txt",
                        "http://localhost/readme1.txt",
                        "http://localhost/readme.txt"
                    ],
                    "first": false,
                    "index": 3,
                    "index0": 2,
                    "last": true,
                    "length": 3,
                    "previtem": "http://localhost/readme1.txt",
                    "revindex": 1,
                    "revindex0": 0
                },
                "ansible_loop_var": "item",
                "changed": true,
                "invocation": {
                    "module_args": {
                         "status_code": [
                            200
                        ],
                        "url": "http://localhost/readme.txt",
                        ...
                    }
                },
                "status": 200,
                "url": "http://localhost/readme.txt"
            }
        ],
        "skipped": false
    }
}

If I leave the second condition in (results.results[ansible_loop.index0].status == 200), I get an error message that I don’t understand:

"msg": "Task failed: Error while evaluating conditional: object of type 'dict' has no attribute 'results'"

Is there a way to actually print the state of the variable while in the loop? Because when I redirect the output to file and then process that with jq, I get (eg.):

$ jq '.results.results[1].status' < ansible.json
404

I’m running Ansible 2.20.1 from a virtualenv on a Debian 13 machine.

What am I doing wrong, please?

First problem: you are attempting to use the wrong feature; until applies to each individual loop item, not to the loop as a whole, so it will retry every single url, not proceed through the list. You should instead be using break_when:.

Second problem: while inside a loop the registered variable is just the result for the current item/attempt, not the full aggregated result.

    - name: try to get a file
      uri:
        url: "{{ item }}"
        dest: "/tmp/data.dat"
        mode: "0644"
      register: results
      ignore_errors: true
      loop:
        - "http://localhost/readme2.txt"
        - "http://localhost/readme1.txt"
        - "http://localhost/readme.txt"
      loop_control:
        break_when: results.status == 200

Thanks a bunch, works like a charm!