How to show a node but return HTTP 404 response
Drupal's built in mechanisms

for delivering 404 pages are useless to you. We can read:


  Page callback functions wanting to report a "page not found" message should return MENU_NOT_FOUND instead of calling drupal_not_found(). However, functions that are invoked in contexts where that return value might not bubble up to menu_execute_active_handler() should call drupal_not_found().


Of course if page callback will return MENU_NOT_FOUND, it will not return any meaningful data. And while calling drupal_not_found() does not always prevent data delivery, comments under function clearly shows that it causes double display of error message more often than any other result - not what you want, either.

Drupal generic header functions

may help you. Especially drupal_add_http_header(). Let me emphasize one thing in it's description:


  $value: The HTTP header value; if equal to FALSE, the specified header is unset. If $name is 'Status', this is expected to be a status code followed by a reason phrase, e.g. "404 Not Found".


So you want to set status to 404:

drupal_add_http_header('Status', '404 Not Found');


It should be enough, after all that's what drupal_deliver_html_page() is doing when you use higher-level 404 calls from previous part of my answer.

Also, you might be interested in logging it like other 404 errors:

 watchdog('page not found', check_plain($_GET['q']), NULL, WATCHDOG_WARNING);


Where to place it?

It depends on what you need to decide if page should be 404 or not. If all you need is the actual data in page, you can go for hook_page_delivery_callback_alter():


  This hook is intended for altering the delivery callback based on information unrelated to the path of the page accessed. For example, it can be used to set the delivery callback based on a HTTP request header (as shown in the code sample). To specify a delivery callback based on path information, use hook_menu() or hook_menu_alter().


emphasis mine.

Let's assume that you need it for nodes

and this means you need a node id. NID is part of the path, so, following suggestion quoted above, you want to use hook_menu_alter().

Let's take a look on how node's menu looks like. The part we want to alter is:

  $items['node/%node'] = array(
    'title callback' =&gt; 'node_page_title',
    'title arguments' =&gt; array(1),

    // The page callback also invokes drupal_set_title() in case
    // the menu router's title is overridden by a menu link.
    'page callback' =&gt; 'node_page_view',
    'page arguments' =&gt; array(1),
    'access callback' =&gt; 'node_access',
    'access arguments' =&gt; array('view', 1),
  );


What you need to do is to alter it to provide your own callback. In that callback:


Perform any logic needed
Set header as you need
Log to watchdog if you want to
Call original callback and return it's result. Render cascade does not explicitly set HTTP 200 later, so your status header should be delivered unaltered. Unless some modules will alter it, of course, but that's something we cannot know.


Last notes

thanks to Ayesh K and MPD:


Sending header might now work in any of the preprocess hooks or just about any tpl.php file. The fact that you could does not mean you should, especially not in tpl files, as it may interfere with caching, but it's worth to remember you are not limited to the ways I described above. For example hook_page_alter() can be a safe choice, too.
Make sure the page size is more than 512 bytes, otherwise some browsers (Howdy, IE) will ignore it.
Keep in mind that by RFC 2616


  User agents SHOULD display any included entity to the user. 


As you may notice, this RFC uses MUST in some cases and this is not one of them, so if browser will refuse to display your page after you send 404, it's not exactly an error. Browsers are not forbidden to ignore your HTML when you send 404.