Repository: lukasschwab/arxiv.py

Issue:
AttributeError: nonexistent IDs in `id_list`s yield invalid entries
# Description
> A clear and concise description of what the bug is.

When a specified ID doesn't correspond to an arXiv paper, the results feed includes an `entry` element missing expected fields (`id`).

The status is 200, but `feedparser` chokes and the error-handling in this package tries to access the nonexistent ID, yielding a raw `AttributeError`

## Steps to reproduce
> Steps to reproduce the behavior; ideally, include a code snippet.

Example API feed: http://export.arxiv.org/api/query?id_list=2208.05394

```pycon
>>> import arxiv
>>> pub = next(arxiv.Search(id_list=["2208.05394"]).get())
Traceback (most recent call last):
  File "/Users/lukas/.pyenv/versions/3.7.9/lib/python3.7/site-packages/feedparser/util.py", line 156, in __getattr__
    return self.__getitem__(key)
  File "/Users/lukas/.pyenv/versions/3.7.9/lib/python3.7/site-packages/feedparser/util.py", line 113, in __getitem__
    return dict.__getitem__(self, key)
KeyError: 'id'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/lukas/.pyenv/versions/3.7.9/lib/python3.7/site-packages/arxiv/arxiv.py", line 586, in results
    yield Result._from_feed_entry(entry)
  File "/Users/lukas/.pyenv/versions/3.7.9/lib/python3.7/site-packages/arxiv/arxiv.py", line 122, in _from_feed_entry
    entry.id
  File "/Users/lukas/.pyenv/versions/3.7.9/lib/python3.7/site-packages/feedparser/util.py", line 158, in __getattr__
    raise AttributeError("object has no attribute '%s'" % key)
AttributeError: object has no attribute 'id'
```

## Expected behavior
> A clear and concise description of what you expected to happen.

This package's error handling should return a neatly handleable error.

# Versions

<!-- Run `python --version`. -->
+ `python` version: 3.7.9

<!-- Run `pip freeze | grep arxiv`. -->
+ `arxiv.py` version: 1.4.1


Discussion:

Patch:
diff --git a/arxiv/arxiv.py b/arxiv/arxiv.py
index e4720c2..263ed9f 100644
--- a/arxiv/arxiv.py
+++ b/arxiv/arxiv.py
@@ -109,6 +109,8 @@ class Result(object):
         Converts a feedparser entry for an arXiv search result feed into a
         Result object.
         """
+        if not hasattr(entry, "id"):
+            raise Result.MissingFieldError("id")
         # Title attribute may be absent for certain titles. Defaulting to "0" as
         # it's the only title observed to cause this bug.
         # https://github.com/lukasschwab/arxiv.py/issues/71
@@ -349,6 +351,27 @@ class Result(object):
                 return self.href == other.href
             return False
 
+    class MissingFieldError(Exception):
+        """
+        An error indicating an entry is unparseable because it lacks required
+        fields.
+        """
+
+        missing_field: str
+        """The required field missing from the would-be entry."""
+        message: str
+        """Message describing what caused this error."""
+
+        def __init__(self, missing_field):
+            self.missing_field = missing_field
+            self.message = "Entry from arXiv missing required info"
+
+        def __repr__(self) -> str:
+            return '{}({})'.format(
+                _classname(self),
+                repr(self.missing_field)
+            )
+
 
 class SortCriterion(Enum):
     """
@@ -583,7 +606,11 @@ class Client(object):
             offset += len(feed.entries)
             # Yield query results until page is exhausted.
             for entry in feed.entries:
-                yield Result._from_feed_entry(entry)
+                try:
+                    yield Result._from_feed_entry(entry)
+                except Result.MissingFieldError:
+                    logger.warning("Skipping partial result")
+                    continue
 
     def _format_url(self, search: Search, start: int, page_size: int) -> str:
         """
@@ -683,7 +710,6 @@ class ArxivError(Exception):
         self.url = url
         self.retry = retry
         self.message = message
-        # logger.info(self.message, extra=extra)
         super().__init__(self.message)
 
     def __str__(self) -> str:
diff --git a/docs/index.html b/docs/index.html
index 88e443e..5c55ca0 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -136,6 +136,21 @@
                             </ul>
 
                         </li>
+                        <li>
+                                <a class="class" href="#Result.MissingFieldError">Result.MissingFieldError</a>
+                                        <ul class="memberlist">
+                                    <li>
+                                            <a class="function" href="#Result.MissingFieldError.__init__">MissingFieldError</a>
+                                    </li>
+                                    <li>
+                                            <a class="variable" href="#Result.MissingFieldError.missing_field">missing_field</a>
+                                    </li>
+                                    <li>
+                                            <a class="variable" href="#Result.MissingFieldError.message">message</a>
+                                    </li>
+                            </ul>
+
+                        </li>
                 </ul>
 
             </li>
@@ -573,6 +588,8 @@ arxiv<wbr>.arxiv    </h1>
 <span class="sd">        Converts a feedparser entry for an arXiv search result feed into a</span>
 <span class="sd">        Result object.</span>
 <span class="sd">        &quot;&quot;&quot;</span>
+        <span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">entry</span><span class="p">,</span> <span class="s2">&quot;id&quot;</span><span class="p">):</span>
+            <span class="k">raise</span> <span class="n">Result</span><span class="o">.</span><span class="n">MissingFieldError</span><span class="p">(</span><span class="s2">&quot;id&quot;</span><span class="p">)</span>
         <span class="c1"># Title attribute may be absent for certain titles. Defaulting to &quot;0&quot; as</span>
         <span class="c1"># it&#39;s the only title observed to cause this bug.</span>
         <span class="c1"># https://github.com/lukasschwab/arxiv.py/issues/71</span>
@@ -813,6 +830,27 @@ arxiv<wbr>.arxiv    </h1>
                 <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">href</span> <span class="o">==</span> <span class="n">other</span><span class="o">.</span><span class="n">href</span>
             <span class="k">return</span> <span class="kc">False</span>
 
+    <span class="k">class</span> <span class="nc">MissingFieldError</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span>
+        <span class="sd">&quot;&quot;&quot;</span>
+<span class="sd">        An error indicating an entry is unparseable because it lacks required</span>
+<span class="sd">        fields.</span>
+<span class="sd">        &quot;&quot;&quot;</span>
+
+        <span class="n">missing_field</span><span class="p">:</span> <span class="nb">str</span>
+        <span class="sd">&quot;&quot;&quot;The required field missing from the would-be entry.&quot;&quot;&quot;</span>
+        <span class="n">message</span><span class="p">:</span> <span class="nb">str</span>
+        <span class="sd">&quot;&quot;&quot;Message describing what caused this error.&quot;&quot;&quot;</span>
+
+        <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">missing_field</span><span class="p">):</span>
+            <span class="bp">self</span><span class="o">.</span><span class="n">missing_field</span> <span class="o">=</span> <span class="n">missing_field</span>
+            <span class="bp">self</span><span class="o">.</span><span class="n">message</span> <span class="o">=</span> <span class="s2">&quot;Entry from arXiv missing required info&quot;</span>
+
+        <span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
+            <span class="k">return</span> <span class="s1">&#39;</span><span class="si">{}</span><span class="s1">(</span><span class="si">{}</span><span class="s1">)&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
+                <span class="n">_classname</span><span class="p">(</span><span class="bp">self</span><span class="p">),</span>
+                <span class="nb">repr</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">missing_field</span><span class="p">)</span>
+            <span class="p">)</span>
+
 
 <span class="k">class</span> <span class="nc">SortCriterion</span><span class="p">(</span><span class="n">Enum</span><span class="p">):</span>
     <span class="sd">&quot;&quot;&quot;</span>
@@ -1047,7 +1085,11 @@ arxiv<wbr>.arxiv    </h1>
             <span class="n">offset</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">feed</span><span class="o">.</span><span class="n">entries</span><span class="p">)</span>
             <span class="c1"># Yield query results until page is exhausted.</span>
             <span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">feed</span><span class="o">.</span><span class="n">entries</span><span class="p">:</span>
-                <span class="k">yield</span> <span class="n">Result</span><span class="o">.</span><span class="n">_from_feed_entry</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span>
+                <span class="k">try</span><span class="p">:</span>
+                    <span class="k">yield</span> <span class="n">Result</span><span class="o">.</span><span class="n">_from_feed_entry</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span>
+                <span class="k">except</span> <span class="n">Result</span><span class="o">.</span><span class="n">MissingFieldError</span><span class="p">:</span>
+                    <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&quot;Skipping partial result&quot;</span><span class="p">)</span>
+                    <span class="k">continue</span>
 
     <span class="k">def</span> <span class="nf">_format_url</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">search</span><span class="p">:</span> <span class="n">Search</span><span class="p">,</span> <span class="n">start</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">page_size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
         <span class="sd">&quot;&quot;&quot;</span>
@@ -1147,7 +1189,6 @@ arxiv<wbr>.arxiv    </h1>
         <span class="bp">self</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="n">url</span>
         <span class="bp">self</span><span class="o">.</span><span class="n">retry</span> <span class="o">=</span> <span class="n">retry</span>
         <span class="bp">self</span><span class="o">.</span><span class="n">message</span> <span class="o">=</span> <span class="n">message</span>
-        <span class="c1"># logger.info(self.message, extra=extra)</span>
         <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">message</span><span class="p">)</span>
 
     <span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
@@ -1331,6 +1372,8 @@ arxiv<wbr>.arxiv    </h1>
 <span class="sd">        Converts a feedparser entry for an arXiv search result feed into a</span>
 <span class="sd">        Result object.</span>
 <span class="sd">        &quot;&quot;&quot;</span>
+        <span class="k">if</span> <span class="ow">not</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">entry</span><span class="p">,</span> <span class="s2">&quot;id&quot;</span><span class="p">):</span>
+            <span class="k">raise</span> <span class="n">Result</span><span class="o">.</span><span class="n">MissingFieldError</span><span class="p">(</span><span class="s2">&quot;id&quot;</span><span class="p">)</span>
         <span class="c1"># Title attribute may be absent for certain titles. Defaulting to &quot;0&quot; as</span>
         <span class="c1"># it&#39;s the only title observed to cause this bug.</span>
         <span class="c1"># https://github.com/lukasschwab/arxiv.py/issues/71</span>
@@ -1570,6 +1613,27 @@ arxiv<wbr>.arxiv    </h1>
             <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">other</span><span class="p">,</span> <span class="n">Result</span><span class="o">.</span><span class="n">Link</span><span class="p">):</span>
                 <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">href</span> <span class="o">==</span> <span class="n">other</span><span class="o">.</span><span class="n">href</span>
             <span class="k">return</span> <span class="kc">False</span>
+
+    <span class="k">class</span> <span class="nc">MissingFieldError</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span>
+        <span class="sd">&quot;&quot;&quot;</span>
+<span class="sd">        An error indicating an entry is unparseable because it lacks required</span>
+<span class="sd">        fields.</span>
+<span class="sd">        &quot;&quot;&quot;</span>
+
+        <span class="n">missing_field</span><span class="p">:</span> <span class="nb">str</span>
+        <span class="sd">&quot;&quot;&quot;The required field missing from the would-be entry.&quot;&quot;&quot;</span>
+        <span class="n">message</span><span class="p">:</span> <span class="nb">str</span>
+        <span class="sd">&quot;&quot;&quot;Message describing what caused this error.&quot;&quot;&quot;</span>
+
+        <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">missing_field</span><span class="p">):</span>
+            <span class="bp">self</span><span class="o">.</span><span class="n">missing_field</span> <span class="o">=</span> <span class="n">missing_field</span>
+            <span class="bp">self</span><span class="o">.</span><span class="n">message</span> <span class="o">=</span> <span class="s2">&quot;Entry from arXiv missing required info&quot;</span>
+
+        <span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
+            <span class="k">return</span> <span class="s1">&#39;</span><span class="si">{}</span><span class="s1">(</span><span class="si">{}</span><span class="s1">)&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
+                <span class="n">_classname</span><span class="p">(</span><span class="bp">self</span><span class="p">),</span>
+                <span class="nb">repr</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">missing_field</span><span class="p">)</span>
+            <span class="p">)</span>
 </pre></div>
 
         </details>
@@ -2184,6 +2248,98 @@ constructing <code><a href="#Result.Link">Link</a></code>s yourself.</p>
 
                             </div>
                 </section>
+                <section id="Result.MissingFieldError">
+                                <div class="attr class">
+        <a class="headerlink" href="#Result.MissingFieldError">#&nbsp;&nbsp</a>
+
+        
+        <span class="def">class</span>
+        <span class="name">Result.MissingFieldError</span><wbr>(<span class="base">builtins.Exception</span>):
+    </div>
+
+                <details>
+            <summary>View Source</summary>
+            <div class="codehilite"><pre><span></span>    <span class="k">class</span> <span class="nc">MissingFieldError</span><span class="p">(</span><span class="ne">Exception</span><span class="p">):</span>
+        <span class="sd">&quot;&quot;&quot;</span>
+<span class="sd">        An error indicating an entry is unparseable because it lacks required</span>
+<span class="sd">        fields.</span>
+<span class="sd">        &quot;&quot;&quot;</span>
+
+        <span class="n">missing_field</span><span class="p">:</span> <span class="nb">str</span>
+        <span class="sd">&quot;&quot;&quot;The required field missing from the would-be entry.&quot;&quot;&quot;</span>
+        <span class="n">message</span><span class="p">:</span> <span class="nb">str</span>
+        <span class="sd">&quot;&quot;&quot;Message describing what caused this error.&quot;&quot;&quot;</span>
+
+        <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">missing_field</span><span class="p">):</span>
+            <span class="bp">self</span><span class="o">.</span><span class="n">missing_field</span> <span class="o">=</span> <span class="n">missing_field</span>
+            <span class="bp">self</span><span class="o">.</span><span class="n">message</span> <span class="o">=</span> <span class="s2">&quot;Entry from arXiv missing required info&quot;</span>
+
+        <span class="k">def</span> <span class="fm">__repr__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
+            <span class="k">return</span> <span class="s1">&#39;</span><span class="si">{}</span><span class="s1">(</span><span class="si">{}</span><span class="s1">)&#39;</span><span class="o">.</span><span class="n">format</span><span class="p">(</span>
+                <span class="n">_classname</span><span class="p">(</span><span class="bp">self</span><span class="p">),</span>
+                <span class="nb">repr</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">missing_field</span><span class="p">)</span>
+            <span class="p">)</span>
+</pre></div>
+
+        </details>
+
+            <div class="docstring"><p>An error indicating an entry is unparseable because it lacks required
+fields.</p>
+</div>
+
+
+                            <div id="Result.MissingFieldError.__init__" class="classattr">
+                                        <div class="attr function"><a class="headerlink" href="#Result.MissingFieldError.__init__">#&nbsp;&nbsp</a>
+
+        
+            <span class="name">Result.MissingFieldError</span><span class="signature">(missing_field)</span>
+    </div>
+
+                <details>
+            <summary>View Source</summary>
+            <div class="codehilite"><pre><span></span>        <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">missing_field</span><span class="p">):</span>
+            <span class="bp">self</span><span class="o">.</span><span class="n">missing_field</span> <span class="o">=</span> <span class="n">missing_field</span>
+            <span class="bp">self</span><span class="o">.</span><span class="n">message</span> <span class="o">=</span> <span class="s2">&quot;Entry from arXiv missing required info&quot;</span>
+</pre></div>
+
+        </details>
+
+    
+
+                            </div>
+                            <div id="Result.MissingFieldError.missing_field" class="classattr">
+                                            <div class="attr variable"><a class="headerlink" href="#Result.MissingFieldError.missing_field">#&nbsp;&nbsp</a>
+
+        <span class="name">missing_field</span><span class="annotation">: str</span>
+    </div>
+
+            <div class="docstring"><p>The required field missing from the would-be entry.</p>
+</div>
+
+
+                            </div>
+                            <div id="Result.MissingFieldError.message" class="classattr">
+                                            <div class="attr variable"><a class="headerlink" href="#Result.MissingFieldError.message">#&nbsp;&nbsp</a>
+
+        <span class="name">message</span><span class="annotation">: str</span>
+    </div>
+
+            <div class="docstring"><p>Message describing what caused this error.</p>
+</div>
+
+
+                            </div>
+                            <div class="inherited">
+                                <h5>Inherited Members</h5>
+                                <dl>
+                                    <div><dt>builtins.BaseException</dt>
+                                <dd id="Result.MissingFieldError.with_traceback" class="function">with_traceback</dd>
+                <dd id="Result.MissingFieldError.args" class="variable">args</dd>
+
+            </div>
+                                </dl>
+                            </div>
+                </section>
                 <section id="SortCriterion">
                                 <div class="attr class">
         <a class="headerlink" href="#SortCriterion">#&nbsp;&nbsp</a>
@@ -2720,7 +2876,11 @@ search.</p>
             <span class="n">offset</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">feed</span><span class="o">.</span><span class="n">entries</span><span class="p">)</span>
             <span class="c1"># Yield query results until page is exhausted.</span>
             <span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">feed</span><span class="o">.</span><span class="n">entries</span><span class="p">:</span>
-                <span class="k">yield</span> <span class="n">Result</span><span class="o">.</span><span class="n">_from_feed_entry</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span>
+                <span class="k">try</span><span class="p">:</span>
+                    <span class="k">yield</span> <span class="n">Result</span><span class="o">.</span><span class="n">_from_feed_entry</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span>
+                <span class="k">except</span> <span class="n">Result</span><span class="o">.</span><span class="n">MissingFieldError</span><span class="p">:</span>
+                    <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&quot;Skipping partial result&quot;</span><span class="p">)</span>
+                    <span class="k">continue</span>
 
     <span class="k">def</span> <span class="nf">_format_url</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">search</span><span class="p">:</span> <span class="n">Search</span><span class="p">,</span> <span class="n">start</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">page_size</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
         <span class="sd">&quot;&quot;&quot;</span>
@@ -2986,7 +3146,11 @@ brittle behavior, and inconsistent results.</p>
             <span class="n">offset</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">feed</span><span class="o">.</span><span class="n">entries</span><span class="p">)</span>
             <span class="c1"># Yield query results until page is exhausted.</span>
             <span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">feed</span><span class="o">.</span><span class="n">entries</span><span class="p">:</span>
-                <span class="k">yield</span> <span class="n">Result</span><span class="o">.</span><span class="n">_from_feed_entry</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span>
+                <span class="k">try</span><span class="p">:</span>
+                    <span class="k">yield</span> <span class="n">Result</span><span class="o">.</span><span class="n">_from_feed_entry</span><span class="p">(</span><span class="n">entry</span><span class="p">)</span>
+                <span class="k">except</span> <span class="n">Result</span><span class="o">.</span><span class="n">MissingFieldError</span><span class="p">:</span>
+                    <span class="n">logger</span><span class="o">.</span><span class="n">warning</span><span class="p">(</span><span class="s2">&quot;Skipping partial result&quot;</span><span class="p">)</span>
+                    <span class="k">continue</span>
 </pre></div>
 
         </details>
@@ -3035,7 +3199,6 @@ have been yielded or there are no more search results.</p>
         <span class="bp">self</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="n">url</span>
         <span class="bp">self</span><span class="o">.</span><span class="n">retry</span> <span class="o">=</span> <span class="n">retry</span>
         <span class="bp">self</span><span class="o">.</span><span class="n">message</span> <span class="o">=</span> <span class="n">message</span>
-        <span class="c1"># logger.info(self.message, extra=extra)</span>
         <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">message</span><span class="p">)</span>
 
     <span class="k">def</span> <span class="fm">__str__</span><span class="p">(</span><span class="bp">self</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
@@ -3064,7 +3227,6 @@ have been yielded or there are no more search results.</p>
         <span class="bp">self</span><span class="o">.</span><span class="n">url</span> <span class="o">=</span> <span class="n">url</span>
         <span class="bp">self</span><span class="o">.</span><span class="n">retry</span> <span class="o">=</span> <span class="n">retry</span>
         <span class="bp">self</span><span class="o">.</span><span class="n">message</span> <span class="o">=</span> <span class="n">message</span>
-        <span class="c1"># logger.info(self.message, extra=extra)</span>
         <span class="nb">super</span><span class="p">()</span><span class="o">.</span><span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">message</span><span class="p">)</span>
 </pre></div>
 
diff --git a/setup.py b/setup.py
index f8e5ab6..67d63c9 100644
--- a/setup.py
+++ b/setup.py
@@ -1,6 +1,6 @@
 from setuptools import setup
 
-version = '1.4.1'
+version = '1.4.2'
 
 with open('README.md', 'r') as fh:
     long_description = fh.read()


Test patch:
diff --git a/tests/test_client.py b/tests/test_client.py
index 7d56e65..5cab9a0 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -3,13 +3,30 @@ from unittest.mock import MagicMock, call, patch
 import arxiv
 from datetime import datetime, timedelta
 from pytest import approx
+import feedparser
 
 
 class TestClient(unittest.TestCase):
+    def test_invalid_format_id(self):
+        with self.assertRaises(arxiv.HTTPError):
+            list(arxiv.Client(num_retries=0).results(arxiv.Search(id_list=["abc"])))
+
     def test_invalid_id(self):
         results = list(arxiv.Search(id_list=["0000.0000"]).results())
         self.assertEqual(len(results), 0)
 
+    def test_nonexistent_id_in_list(self):
+        # Assert _from_feed_entry throws MissingFieldError.
+        feed = feedparser.parse("http://export.arxiv.org/api/query?id_list=0808.05394")
+        with self.assertRaises(arxiv.Result.MissingFieldError):
+            arxiv.Result._from_feed_entry(feed.entries[0])
+        # Assert thrown error is handled and hidden by generator.
+        results = list(arxiv.Search(id_list=["0808.05394"]).results())
+        self.assertEqual(len(results), 0)
+        # Generator should still yield valid entries.
+        results = list(arxiv.Search(id_list=["0808.05394", "1707.08567"]).results())
+        self.assertEqual(len(results), 1)
+
     def test_max_results(self):
         client = arxiv.Client(page_size=10, delay_seconds=0)
         search = arxiv.Search(query="testing", max_results=2)
@@ -33,7 +50,7 @@ class TestClient(unittest.TestCase):
 
     @patch('time.sleep', return_value=None)
     def test_retry(self, patched_time_sleep):
-        broken_client = get_broken_client()
+        broken_client = TestClient.get_broken_client()
 
         def broken_get():
             search = arxiv.Search(query="quantum")
@@ -107,7 +124,7 @@ class TestClient(unittest.TestCase):
 
     @patch('time.sleep', return_value=None)
     def test_sleep_between_errors(self, patched_time_sleep):
-        client = get_broken_client()
+        client = TestClient.get_broken_client()
         url = client._format_url(arxiv.Search(query="quantum"), 0, 1)
         try:
             client._parse_feed(url)
@@ -120,9 +137,18 @@ class TestClient(unittest.TestCase):
             call(approx(client.delay_seconds, rel=1e-3)),
         ] * client.num_retries)
 
+    def get_broken_client():
+        """
+        get_broken_client returns an arxiv.Client that always encounters a 500
+        status.
+        """
+        # TODO: reimplement broken_client with a mock.
+        broken_client = arxiv.Client(page_size=1)
+        broken_client.query_url_format = "https://httpstat.us/500?{}"
+        return broken_client
 
-def get_broken_client():
-    # broken_client always encounters a 500 status.
-    broken_client = arxiv.Client(page_size=1)
-    broken_client.query_url_format = "https://httpstat.us/500?{}"
-    return broken_client
+    def get_once_client():
+        """
+        get_once_client returns an arxiv.Client that only tries once.
+        """
+        return arxiv.Client(num_retries=0)